diff --git a/device.mk b/device.mk index c1f6a56..e3c47d7 100644 --- a/device.mk +++ b/device.mk @@ -115,6 +115,10 @@ PRODUCT_PACKAGES += \ android.hardware.ir@1.0-impl \ android.hardware.ir@1.0-service +# Device-specific settings +PRODUCT_PACKAGES += \ + XiaomiParts + # Dex PRODUCT_ART_TARGET_INCLUDE_DEBUG_BUILD := false PRODUCT_DEX_PREOPT_DEFAULT_COMPILER_FILTER := verify diff --git a/parts/Android.bp b/parts/Android.bp new file mode 100644 index 0000000..82c6e13 --- /dev/null +++ b/parts/Android.bp @@ -0,0 +1,25 @@ +// +// Copyright (C) 2017-2020 The LineageOS Project +// +// SPDX-License-Identifier: Apache-2.0 +// + +android_app { + name: "XiaomiParts", + + srcs: ["src/**/*.java"], + + certificate: "platform", + platform_apis: true, + system_ext_specific: true, + privileged: true, + + static_libs: [ + "org.lineageos.settings.resources", + ], + + optimize: { + proguard_flags_files: ["proguard.flags"], + }, + +} diff --git a/parts/AndroidManifest.xml b/parts/AndroidManifest.xml new file mode 100644 index 0000000..cdd3f84 --- /dev/null +++ b/parts/AndroidManifest.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/parts/proguard.flags b/parts/proguard.flags new file mode 100644 index 0000000..e69de29 diff --git a/parts/res/drawable/ic_clear_speaker.xml b/parts/res/drawable/ic_clear_speaker.xml new file mode 100644 index 0000000..4e2981d --- /dev/null +++ b/parts/res/drawable/ic_clear_speaker.xml @@ -0,0 +1,13 @@ + + + + diff --git a/parts/res/drawable/ic_refresh_120.xml b/parts/res/drawable/ic_refresh_120.xml new file mode 100644 index 0000000..f81418b --- /dev/null +++ b/parts/res/drawable/ic_refresh_120.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/parts/res/drawable/ic_refresh_60.xml b/parts/res/drawable/ic_refresh_60.xml new file mode 100644 index 0000000..2c4a62f --- /dev/null +++ b/parts/res/drawable/ic_refresh_60.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/parts/res/drawable/ic_refresh_90.xml b/parts/res/drawable/ic_refresh_90.xml new file mode 100644 index 0000000..71ea123 --- /dev/null +++ b/parts/res/drawable/ic_refresh_90.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/parts/res/drawable/ic_refresh_default.xml b/parts/res/drawable/ic_refresh_default.xml new file mode 100644 index 0000000..a4b7d3a --- /dev/null +++ b/parts/res/drawable/ic_refresh_default.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/parts/res/drawable/ic_thermal_benchmark.xml b/parts/res/drawable/ic_thermal_benchmark.xml new file mode 100644 index 0000000..8b3f2c1 --- /dev/null +++ b/parts/res/drawable/ic_thermal_benchmark.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/parts/res/drawable/ic_thermal_browser.xml b/parts/res/drawable/ic_thermal_browser.xml new file mode 100644 index 0000000..73880a0 --- /dev/null +++ b/parts/res/drawable/ic_thermal_browser.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/parts/res/drawable/ic_thermal_camera.xml b/parts/res/drawable/ic_thermal_camera.xml new file mode 100644 index 0000000..642c082 --- /dev/null +++ b/parts/res/drawable/ic_thermal_camera.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/parts/res/drawable/ic_thermal_default.xml b/parts/res/drawable/ic_thermal_default.xml new file mode 100644 index 0000000..cc78c36 --- /dev/null +++ b/parts/res/drawable/ic_thermal_default.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/parts/res/drawable/ic_thermal_dialer.xml b/parts/res/drawable/ic_thermal_dialer.xml new file mode 100644 index 0000000..f87e39b --- /dev/null +++ b/parts/res/drawable/ic_thermal_dialer.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/parts/res/drawable/ic_thermal_gaming.xml b/parts/res/drawable/ic_thermal_gaming.xml new file mode 100644 index 0000000..29a13b8 --- /dev/null +++ b/parts/res/drawable/ic_thermal_gaming.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/parts/res/drawable/ic_thermal_streaming.xml b/parts/res/drawable/ic_thermal_streaming.xml new file mode 100644 index 0000000..e6e272b --- /dev/null +++ b/parts/res/drawable/ic_thermal_streaming.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/parts/res/layout/refresh_layout.xml b/parts/res/layout/refresh_layout.xml new file mode 100644 index 0000000..6467efa --- /dev/null +++ b/parts/res/layout/refresh_layout.xml @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/parts/res/layout/refresh_list_item.xml b/parts/res/layout/refresh_list_item.xml new file mode 100644 index 0000000..e2ce15f --- /dev/null +++ b/parts/res/layout/refresh_list_item.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + diff --git a/parts/res/layout/thermal_layout.xml b/parts/res/layout/thermal_layout.xml new file mode 100644 index 0000000..3982a35 --- /dev/null +++ b/parts/res/layout/thermal_layout.xml @@ -0,0 +1,18 @@ + + + diff --git a/parts/res/layout/thermal_list_item.xml b/parts/res/layout/thermal_list_item.xml new file mode 100644 index 0000000..864b7d9 --- /dev/null +++ b/parts/res/layout/thermal_list_item.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + diff --git a/parts/res/raw/clear_speaker_sound.mp3 b/parts/res/raw/clear_speaker_sound.mp3 new file mode 100644 index 0000000..a67b135 Binary files /dev/null and b/parts/res/raw/clear_speaker_sound.mp3 differ diff --git a/parts/res/values/strings.xml b/parts/res/values/strings.xml new file mode 100644 index 0000000..7f33677 --- /dev/null +++ b/parts/res/values/strings.xml @@ -0,0 +1,50 @@ + + + + + + Touch Responsiveness + Increase Touch Responsiveness + Increases touch polling rate to decrease latency + + + Thermal Profiles + Adjust per-app thermal profiles for optimum performance + Default + Benchmark + Browser + Camera + Dialer + Gaming + Streaming + + + Refresh Rate + + + Clear speaker + Play a 30-second audio to clear the speaker + Run this feature once or twice if you find that your speaker is lightly blocked by dust. Set media volume to maximum.\n\nIf the speaker is blocked heavily, run this feature 2-5 times while shaking your device with the speaker facing downwards.\n\nWARNING: Ensure that any headphones are unplugged. + + + Per-app refresh rate + Set the maximum refresh rate for a specific application + Default + 60Hz + 90Hz + 120Hz + diff --git a/parts/res/values/themes.xml b/parts/res/values/themes.xml new file mode 100644 index 0000000..a4438d3 --- /dev/null +++ b/parts/res/values/themes.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/parts/res/xml/clear_speaker_settings.xml b/parts/res/xml/clear_speaker_settings.xml new file mode 100644 index 0000000..8716efb --- /dev/null +++ b/parts/res/xml/clear_speaker_settings.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/parts/src/org/lineageos/settings/BootCompletedReceiver.java b/parts/src/org/lineageos/settings/BootCompletedReceiver.java new file mode 100644 index 0000000..5ca435b --- /dev/null +++ b/parts/src/org/lineageos/settings/BootCompletedReceiver.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * 2017-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; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import org.lineageos.settings.thermal.ThermalUtils; +import org.lineageos.settings.refreshrate.RefreshUtils; + +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"); + ThermalUtils.startService(context); + RefreshUtils.initialize(context); + } +} diff --git a/parts/src/org/lineageos/settings/refreshrate/RefreshActivity.java b/parts/src/org/lineageos/settings/refreshrate/RefreshActivity.java new file mode 100644 index 0000000..2ecf2a4 --- /dev/null +++ b/parts/src/org/lineageos/settings/refreshrate/RefreshActivity.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020-2022 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.refreshrate; + +import android.os.Bundle; + +import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity; +import com.android.settingslib.widget.R; + +public class RefreshActivity extends CollapsingToolbarBaseActivity { + private static final String TAG_REFRESH = "refresh"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getFragmentManager().beginTransaction().replace(R.id.content_frame, + new RefreshSettingsFragment(), TAG_REFRESH).commit(); + } +} diff --git a/parts/src/org/lineageos/settings/refreshrate/RefreshService.java b/parts/src/org/lineageos/settings/refreshrate/RefreshService.java new file mode 100644 index 0000000..fff9e69 --- /dev/null +++ b/parts/src/org/lineageos/settings/refreshrate/RefreshService.java @@ -0,0 +1,117 @@ +/* + * 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.refreshrate; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.ActivityTaskManager.RootTaskInfo; +import android.app.IActivityTaskManager; +import android.app.Service; +import android.app.TaskStackListener; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.IBinder; +import android.util.Log; +import android.os.RemoteException; + +public class RefreshService extends Service { + + private static final String TAG = "RefreshService"; + private static final boolean DEBUG = false; + + private String mPreviousApp; + private RefreshUtils mRefreshUtils; + private IActivityTaskManager mActivityTaskManager; + private final TaskStackListener mTaskListener = new TaskStackListener() { + @Override + public void onTaskStackChanged() { + try { + final RootTaskInfo info = mActivityTaskManager.getFocusedRootTaskInfo(); + if (info == null || info.topActivity == null) { + return; + } + String foregroundApp = info.topActivity.getPackageName(); + if (!foregroundApp.equals(mPreviousApp)) { + mRefreshUtils.setRefreshRate(foregroundApp); + mPreviousApp = foregroundApp; + } + } catch (Exception e) {} + } + }; + + private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mPreviousApp = ""; + mRefreshUtils.setDefaultRefreshRate(context); + } + }; + + @Override + public void onCreate() { + if (DEBUG) Log.d(TAG, "Creating service"); + mRefreshUtils = new RefreshUtils(this); + mRefreshUtils.setDefaultRefreshRate(this); + try { + mActivityTaskManager = ActivityTaskManager.getService(); + mActivityTaskManager.registerTaskStackListener(mTaskListener); + } catch (RemoteException e) { + // Do nothing + } + registerReceiver(); + super.onCreate(); + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "Destroying service"); + unregisterReceiver(); + try { + ActivityTaskManager.getService().unregisterTaskStackListener(mTaskListener); + } catch (RemoteException e) { + // Do nothing + } + mRefreshUtils.setDefaultRefreshRate(this); + super.onDestroy(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (DEBUG) Log.d(TAG, "Starting service"); + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void registerReceiver() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_SCREEN_ON); + this.registerReceiver(mIntentReceiver, filter); + } + + private void unregisterReceiver() { + this.unregisterReceiver(mIntentReceiver); + } +} diff --git a/parts/src/org/lineageos/settings/refreshrate/RefreshSettingsFragment.java b/parts/src/org/lineageos/settings/refreshrate/RefreshSettingsFragment.java new file mode 100644 index 0000000..f43a8ac --- /dev/null +++ b/parts/src/org/lineageos/settings/refreshrate/RefreshSettingsFragment.java @@ -0,0 +1,421 @@ +/** + * 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.refreshrate; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.SectionIndexer; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceFragment; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.android.settingslib.applications.ApplicationsState; + +import org.lineageos.settings.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RefreshSettingsFragment extends PreferenceFragment + implements ApplicationsState.Callbacks { + + private AllPackagesAdapter mAllPackagesAdapter; + private ApplicationsState mApplicationsState; + private ApplicationsState.Session mSession; + private ActivityFilter mActivityFilter; + private Map mEntryMap = + new HashMap(); + + private RefreshUtils mRefreshUtils; + private RecyclerView mAppsRecyclerView; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mApplicationsState = ApplicationsState.getInstance(getActivity().getApplication()); + mSession = mApplicationsState.newSession(this); + mSession.onResume(); + mActivityFilter = new ActivityFilter(getActivity().getPackageManager()); + + mAllPackagesAdapter = new AllPackagesAdapter(getActivity()); + + mRefreshUtils = new RefreshUtils(getActivity()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.refresh_layout, container, false); + } + + @Override + public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mAppsRecyclerView = view.findViewById(R.id.refresh_rv_view); + mAppsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + mAppsRecyclerView.setAdapter(mAllPackagesAdapter); + } + + + @Override + public void onResume() { + super.onResume(); + rebuild(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + mSession.onPause(); + mSession.onDestroy(); + } + + @Override + public void onPackageListChanged() { + mActivityFilter.updateLauncherInfoList(); + rebuild(); + } + + @Override + public void onRebuildComplete(ArrayList entries) { + if (entries != null) { + handleAppEntries(entries); + mAllPackagesAdapter.notifyDataSetChanged(); + } + } + + @Override + public void onLoadEntriesCompleted() { + rebuild(); + } + + @Override + public void onAllSizesComputed() { + } + + @Override + public void onLauncherInfoChanged() { + } + + @Override + public void onPackageIconChanged() { + } + + @Override + public void onPackageSizeChanged(String packageName) { + } + + @Override + public void onRunningStateChanged(boolean running) { + } + + private void handleAppEntries(List entries) { + final ArrayList sections = new ArrayList(); + final ArrayList positions = new ArrayList(); + final PackageManager pm = getActivity().getPackageManager(); + String lastSectionIndex = null; + int offset = 0; + + for (int i = 0; i < entries.size(); i++) { + final ApplicationInfo info = entries.get(i).info; + final String label = (String) info.loadLabel(pm); + final String sectionIndex; + + if (!info.enabled) { + sectionIndex = "--"; // XXX + } else if (TextUtils.isEmpty(label)) { + sectionIndex = ""; + } else { + sectionIndex = label.substring(0, 1).toUpperCase(); + } + + if (lastSectionIndex == null || + !TextUtils.equals(sectionIndex, lastSectionIndex)) { + sections.add(sectionIndex); + positions.add(offset); + lastSectionIndex = sectionIndex; + } + + offset++; + } + + mAllPackagesAdapter.setEntries(entries, sections, positions); + mEntryMap.clear(); + for (ApplicationsState.AppEntry e : entries) { + mEntryMap.put(e.info.packageName, e); + } + } + + private void rebuild() { + mSession.rebuild(mActivityFilter, ApplicationsState.ALPHA_COMPARATOR); + } + + private int getStateDrawable(int state) { + switch (state) { + case RefreshUtils.STATE_STANDARD: + return R.drawable.ic_refresh_60; + case RefreshUtils.STATE_HIGH: + return R.drawable.ic_refresh_90; + case RefreshUtils.STATE_EXTREME: + return R.drawable.ic_refresh_120; + case RefreshUtils.STATE_DEFAULT: + default: + return R.drawable.ic_refresh_default; + } + } + + private class ViewHolder extends RecyclerView.ViewHolder { + private TextView title; + private Spinner mode; + private ImageView icon; + private View rootView; + private ImageView stateIcon; + + private ViewHolder(View view) { + super(view); + this.title = view.findViewById(R.id.app_name); + this.mode = view.findViewById(R.id.app_mode); + this.icon = view.findViewById(R.id.app_icon); + this.stateIcon = view.findViewById(R.id.state); + this.rootView = view; + + view.setTag(this); + } + } + + private class ModeAdapter extends BaseAdapter { + + private final LayoutInflater inflater; + private final int[] items = { + R.string.refresh_default, + R.string.refresh_medium, + R.string.refresh_high, + R.string.refresh_extreme + }; + + private ModeAdapter(Context context) { + inflater = LayoutInflater.from(context); + } + + @Override + public int getCount() { + return items.length; + } + + @Override + public Object getItem(int position) { + return items[position]; + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView view; + if (convertView != null) { + view = (TextView) convertView; + } else { + view = (TextView) inflater.inflate(android.R.layout.simple_spinner_dropdown_item, + parent, false); + } + + view.setText(items[position]); + view.setTextSize(14f); + + return view; + } + } + + private class AllPackagesAdapter extends RecyclerView.Adapter + implements AdapterView.OnItemSelectedListener, SectionIndexer { + + private List mEntries = new ArrayList<>(); + private String[] mSections; + private int[] mPositions; + + public AllPackagesAdapter(Context context) { + mActivityFilter = new ActivityFilter(context.getPackageManager()); + } + + @Override + public int getItemCount() { + return mEntries.size(); + } + + @Override + public long getItemId(int position) { + return mEntries.get(position).id; + } +@NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.refresh_list_item, parent, false)); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Context context = holder.itemView.getContext(); + + ApplicationsState.AppEntry entry = mEntries.get(position); + + if (entry == null) { + return; + } + holder.mode.setAdapter(new ModeAdapter(context)); + holder.mode.setOnItemSelectedListener(this); + holder.title.setText(entry.label); + holder.title.setOnClickListener(v -> holder.mode.performClick()); + mApplicationsState.ensureIcon(entry); + holder.icon.setImageDrawable(entry.icon); + int packageState = mRefreshUtils.getStateForPackage(entry.info.packageName); + holder.mode.setSelection(packageState, false); + holder.mode.setTag(entry); + holder.stateIcon.setImageResource(getStateDrawable(packageState)); + } + + private void setEntries(List entries, + List sections, List positions) { + mEntries = entries; + mSections = sections.toArray(new String[sections.size()]); + mPositions = new int[positions.size()]; + for (int i = 0; i < positions.size(); i++) { + mPositions[i] = positions.get(i); + } + notifyDataSetChanged(); + } + + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + final ApplicationsState.AppEntry entry = (ApplicationsState.AppEntry) parent.getTag(); + + int currentState = mRefreshUtils.getStateForPackage(entry.info.packageName); + if (currentState != position) { + mRefreshUtils.writePackage(entry.info.packageName, position); + notifyDataSetChanged(); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + + @Override + public int getPositionForSection(int section) { + if (section < 0 || section >= mSections.length) { + return -1; + } + + return mPositions[section]; + } + + @Override + public int getSectionForPosition(int position) { + if (position < 0 || position >= getItemCount()) { + return -1; + } + + final int index = Arrays.binarySearch(mPositions, position); + + /* + * Consider this example: section positions are 0, 3, 5; the supplied + * position is 4. The section corresponding to position 4 starts at + * position 3, so the expected return value is 1. Binary search will not + * find 4 in the array and thus will return -insertPosition-1, i.e. -3. + * To get from that number to the expected value of 1 we need to negate + * and subtract 2. + */ + return index >= 0 ? index : -index - 2; + } + + @Override + public Object[] getSections() { + return mSections; + } + } + + private class ActivityFilter implements ApplicationsState.AppFilter { + + private final PackageManager mPackageManager; + private final List mLauncherResolveInfoList = new ArrayList(); + + private ActivityFilter(PackageManager packageManager) { + this.mPackageManager = packageManager; + + updateLauncherInfoList(); + } + + public void updateLauncherInfoList() { + Intent i = new Intent(Intent.ACTION_MAIN); + i.addCategory(Intent.CATEGORY_LAUNCHER); + List resolveInfoList = mPackageManager.queryIntentActivities(i, 0); + + synchronized (mLauncherResolveInfoList) { + mLauncherResolveInfoList.clear(); + for (ResolveInfo ri : resolveInfoList) { + mLauncherResolveInfoList.add(ri.activityInfo.packageName); + } + } + } + + @Override + public void init() { + } + + @Override + public boolean filterApp(ApplicationsState.AppEntry entry) { + boolean show = !mAllPackagesAdapter.mEntries.contains(entry.info.packageName); + if (show) { + synchronized (mLauncherResolveInfoList) { + show = mLauncherResolveInfoList.contains(entry.info.packageName); + } + } + return show; + } + } +} diff --git a/parts/src/org/lineageos/settings/refreshrate/RefreshUtils.java b/parts/src/org/lineageos/settings/refreshrate/RefreshUtils.java new file mode 100644 index 0000000..cce0a2e --- /dev/null +++ b/parts/src/org/lineageos/settings/refreshrate/RefreshUtils.java @@ -0,0 +1,181 @@ +/* + * 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.refreshrate; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.UserHandle; +import android.view.Display; + +import android.provider.Settings; +import androidx.preference.PreferenceManager; + +public final class RefreshUtils { + + private static final String REFRESH_CONTROL = "refresh_control"; + private static final String REFRESH_SERVICE = "refresh_service"; + + private static final String KEY_PEAK_REFRESH_RATE = "peak_refresh_rate"; + private static final String KEY_MIN_REFRESH_RATE = "min_refresh_rate"; + private Context mContext; + + protected static final int STATE_DEFAULT = 0; + protected static final int STATE_STANDARD = 1; + protected static final int STATE_HIGH = 2; + protected static final int STATE_EXTREME = 3; + + private static final float REFRESH_STATE_DEFAULT = 120f; + private static final float REFRESH_STATE_STANDARD = 60f; + private static final float REFRESH_STATE_HIGH = 90f; + private static final float REFRESH_STATE_EXTREME = 120f; + + private static final String REFRESH_STANDARD = "refresh.standard="; + private static final String REFRESH_HIGH = "refresh.high="; + private static final String REFRESH_EXTREME = "refresh.extreme="; + + private static boolean isAppInList = false; + private static float defaultMaxRate; + private static float defaultMinRate; + + private SharedPreferences mSharedPrefs; + + protected RefreshUtils(Context context) { + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); + mContext = context; + } + + public static void initialize(Context context) { + defaultMaxRate = Settings.System.getFloat(context.getContentResolver(), KEY_PEAK_REFRESH_RATE, REFRESH_STATE_DEFAULT); + defaultMinRate = Settings.System.getFloat(context.getContentResolver(), KEY_MIN_REFRESH_RATE, REFRESH_STATE_DEFAULT); + + if (isServiceEnabled(context)) + startService(context); + else + setDefaultRefreshRate(context); + } + + public static void startService(Context context) { + context.startServiceAsUser(new Intent(context, RefreshService.class), + UserHandle.CURRENT); + PreferenceManager.getDefaultSharedPreferences(context).edit().putString(REFRESH_SERVICE, "true").apply(); + } + + protected static void stopService(Context context) { + context.stopService(new Intent(context, RefreshService.class)); + PreferenceManager.getDefaultSharedPreferences(context).edit().putString(REFRESH_SERVICE, "false").apply(); + } + + protected static boolean isServiceEnabled(Context context) { + return true; + } + + private void writeValue(String profiles) { + mSharedPrefs.edit().putString(REFRESH_CONTROL, profiles).apply(); + } + + private String getValue() { + String value = mSharedPrefs.getString(REFRESH_CONTROL, null); + + if (value == null || value.isEmpty()) { + value = REFRESH_STANDARD + ":" + REFRESH_HIGH + ":" + REFRESH_EXTREME; + writeValue(value); + } + return value; + } + + protected void writePackage(String packageName, int mode) { + String value = getValue(); + value = value.replace(packageName + ",", ""); + String[] modes = value.split(":"); + String finalString; + + switch (mode) { + case STATE_STANDARD: + modes[0] = modes[0] + packageName + ","; + break; + case STATE_HIGH: + modes[1] = modes[1] + packageName + ","; + break; + case STATE_EXTREME: + modes[2] = modes[2] + packageName + ","; + break; + } + + finalString = modes[0] + ":" + modes[1] + ":" + modes[2]; + + writeValue(finalString); + } + + protected int getStateForPackage(String packageName) { + String value = getValue(); + String[] modes = value.split(":"); + int state = STATE_DEFAULT; + if (modes[0].contains(packageName + ",")) { + state = STATE_STANDARD; + } else if (modes[1].contains(packageName + ",")) { + state = STATE_HIGH; + } else if (modes[2].contains(packageName + ",")) { + state = STATE_EXTREME; + } + return state; + } + + protected static void setDefaultRefreshRate(Context context) { + Settings.System.putFloat(context.getContentResolver(), KEY_PEAK_REFRESH_RATE, defaultMaxRate); + Settings.System.putFloat(context.getContentResolver(), KEY_MIN_REFRESH_RATE, defaultMinRate); + } + + protected void setRefreshRate(String packageName) { + String value = getValue(); + String modes[]; + + if (!isAppInList) { + defaultMaxRate = Settings.System.getFloat(mContext.getContentResolver(), KEY_PEAK_REFRESH_RATE, REFRESH_STATE_DEFAULT); + defaultMinRate = Settings.System.getFloat(mContext.getContentResolver(), KEY_MIN_REFRESH_RATE, REFRESH_STATE_DEFAULT); + } + + float minrate = defaultMinRate; + float maxrate = defaultMaxRate; + + if (value != null) { + modes = value.split(":"); + if (modes[0].contains(packageName + ",")) { + maxrate = REFRESH_STATE_STANDARD; + if (minrate > maxrate) { + minrate = maxrate; + } + isAppInList = true; + } else if (modes[1].contains(packageName + ",")) { + maxrate = REFRESH_STATE_HIGH; + if (minrate > maxrate) { + minrate = maxrate; + } + } else if (modes[2].contains(packageName + ",")) { + maxrate = REFRESH_STATE_EXTREME; + if (minrate > maxrate) { + minrate = maxrate; + } + isAppInList = true; + } else { + isAppInList = false; + } + } + Settings.System.putFloat(mContext.getContentResolver(), KEY_PEAK_REFRESH_RATE, maxrate); + Settings.System.putFloat(mContext.getContentResolver(), KEY_MIN_REFRESH_RATE, minrate); + } +} diff --git a/parts/src/org/lineageos/settings/speaker/ClearSpeakerActivity.java b/parts/src/org/lineageos/settings/speaker/ClearSpeakerActivity.java new file mode 100644 index 0000000..d1e74a4 --- /dev/null +++ b/parts/src/org/lineageos/settings/speaker/ClearSpeakerActivity.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 Paranoid Android + * + * 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.speaker; + +import android.os.Bundle; + +import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity; +import com.android.settingslib.widget.R; + +public class ClearSpeakerActivity extends CollapsingToolbarBaseActivity { + + private static final String TAG_CLEARSPEAKER = "clearspeaker"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getFragmentManager().beginTransaction().replace(R.id.content_frame, + new ClearSpeakerFragment(), TAG_CLEARSPEAKER).commit(); + } +} diff --git a/parts/src/org/lineageos/settings/speaker/ClearSpeakerFragment.java b/parts/src/org/lineageos/settings/speaker/ClearSpeakerFragment.java new file mode 100644 index 0000000..4dad735 --- /dev/null +++ b/parts/src/org/lineageos/settings/speaker/ClearSpeakerFragment.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2020 Paranoid Android + * + * 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.speaker; + +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.media.AudioManager; +import android.media.AudioAttributes; +import android.media.MediaPlayer; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import androidx.preference.Preference; +import androidx.preference.PreferenceFragment; +import androidx.preference.SwitchPreference; + +import org.lineageos.settings.R; + +import java.io.IOException; + +public class ClearSpeakerFragment extends PreferenceFragment implements + Preference.OnPreferenceChangeListener { + + private static final String TAG = ClearSpeakerFragment.class.getSimpleName(); + + private static final String PREF_CLEAR_SPEAKER = "clear_speaker_pref"; + + private AudioManager mAudioManager; + private Handler mHandler; + private MediaPlayer mMediaPlayer; + private SwitchPreference mClearSpeakerPref; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.clear_speaker_settings); + + mClearSpeakerPref = (SwitchPreference) findPreference(PREF_CLEAR_SPEAKER); + mClearSpeakerPref.setOnPreferenceChangeListener(this); + + mHandler = new Handler(); + mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference == mClearSpeakerPref) { + boolean value = (Boolean) newValue; + if (value) { + if (startPlaying()) { + mHandler.removeCallbacksAndMessages(null); + mHandler.postDelayed(() -> { + stopPlaying(); + }, 30000); + return true; + } + } + } + return false; + } + + @Override + public void onStop() { + super.onStop(); + stopPlaying(); + } + + public boolean startPlaying() { + mAudioManager.setParameters("status_earpiece_clean=on"); + mMediaPlayer = new MediaPlayer(); + getActivity().setVolumeControlStream(AudioManager.STREAM_MUSIC); + mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mMediaPlayer.setLooping(true); + try { + AssetFileDescriptor file = getResources().openRawResourceFd(R.raw.clear_speaker_sound); + try { + mMediaPlayer.setDataSource(file.getFileDescriptor(), file.getStartOffset(), file.getLength()); + } finally { + file.close(); + } + mClearSpeakerPref.setEnabled(false); + mMediaPlayer.setVolume(1.0f, 1.0f); + mMediaPlayer.prepare(); + mMediaPlayer.start(); + } catch (IOException ioe) { + Log.e(TAG, "Failed to play speaker clean sound!", ioe); + return false; + } + return true; + } + + public void stopPlaying() { + if (mMediaPlayer != null) { + if (mMediaPlayer.isPlaying()) { + mMediaPlayer.stop(); + mMediaPlayer.reset(); + mMediaPlayer.release(); + mMediaPlayer=null; + } + } + mAudioManager.setParameters("status_earpiece_clean=off"); + mClearSpeakerPref.setEnabled(true); + mClearSpeakerPref.setChecked(false); + } +} diff --git a/parts/src/org/lineageos/settings/thermal/ThermalActivity.java b/parts/src/org/lineageos/settings/thermal/ThermalActivity.java new file mode 100644 index 0000000..cb02489 --- /dev/null +++ b/parts/src/org/lineageos/settings/thermal/ThermalActivity.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020,2022 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.thermal; + +import android.os.Bundle; + +import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity; +import com.android.settingslib.widget.R; + +public class ThermalActivity extends CollapsingToolbarBaseActivity { + private static final String TAG_THERMAL = "thermal"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getFragmentManager().beginTransaction().replace(R.id.content_frame, + new ThermalSettingsFragment(), TAG_THERMAL).commit(); + } +} diff --git a/parts/src/org/lineageos/settings/thermal/ThermalService.java b/parts/src/org/lineageos/settings/thermal/ThermalService.java new file mode 100644 index 0000000..cd8e1c1 --- /dev/null +++ b/parts/src/org/lineageos/settings/thermal/ThermalService.java @@ -0,0 +1,79 @@ +/* + * 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.thermal; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.TaskStackListener; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +public class ThermalService extends Service { + + private static final String TAG = "ThermalService"; + private static final boolean DEBUG = false; + + private String mPreviousApp; + private ThermalUtils mThermalUtils; + + @Override + public void onCreate() { + if (DEBUG) Log.d(TAG, "Creating service"); + try { + ActivityTaskManager.getService().registerTaskStackListener(mTaskListener); + } catch (RemoteException e) { + // Do nothing + } + mThermalUtils = new ThermalUtils(this); + super.onCreate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (DEBUG) Log.d(TAG, "Starting service"); + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private final TaskStackListener mTaskListener = new TaskStackListener() { + @Override + public void onTaskStackChanged() { + try { + final ActivityTaskManager.RootTaskInfo focusedTask = + ActivityTaskManager.getService().getFocusedRootTaskInfo(); + if (focusedTask != null && focusedTask.topActivity != null) { + ComponentName taskComponentName = focusedTask.topActivity; + String foregroundApp = taskComponentName.getPackageName(); + if (DEBUG) Log.d(TAG, "onTaskStackChanged: foregroundApp=" + foregroundApp); + if (!foregroundApp.equals(mPreviousApp)) { + mThermalUtils.setThermalProfile(foregroundApp); + mPreviousApp = foregroundApp; + } + } + } catch (Exception e) {} + } + }; +} diff --git a/parts/src/org/lineageos/settings/thermal/ThermalSettingsFragment.java b/parts/src/org/lineageos/settings/thermal/ThermalSettingsFragment.java new file mode 100644 index 0000000..7618b94 --- /dev/null +++ b/parts/src/org/lineageos/settings/thermal/ThermalSettingsFragment.java @@ -0,0 +1,428 @@ +/** + * 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.thermal; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.SectionIndexer; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceFragment; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.android.settingslib.applications.ApplicationsState; + +import org.lineageos.settings.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ThermalSettingsFragment extends PreferenceFragment + implements ApplicationsState.Callbacks { + + private AllPackagesAdapter mAllPackagesAdapter; + private ApplicationsState mApplicationsState; + private ApplicationsState.Session mSession; + private ActivityFilter mActivityFilter; + private Map mEntryMap = + new HashMap(); + + private RecyclerView mAppsRecyclerView; + private ThermalUtils mThermalUtils; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mApplicationsState = ApplicationsState.getInstance(getActivity().getApplication()); + mSession = mApplicationsState.newSession(this); + mSession.onResume(); + mActivityFilter = new ActivityFilter(getActivity().getPackageManager()); + + mAllPackagesAdapter = new AllPackagesAdapter(getActivity()); + + mThermalUtils = new ThermalUtils(getActivity()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.thermal_layout, container, false); + } + + @Override + public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mAppsRecyclerView = view.findViewById(R.id.thermal_rv_view); + mAppsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + mAppsRecyclerView.setAdapter(mAllPackagesAdapter); + } + + + @Override + public void onResume() { + super.onResume(); + rebuild(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + mSession.onPause(); + mSession.onDestroy(); + } + + @Override + public void onPackageListChanged() { + mActivityFilter.updateLauncherInfoList(); + rebuild(); + } + + @Override + public void onRebuildComplete(ArrayList entries) { + if (entries != null) { + handleAppEntries(entries); + mAllPackagesAdapter.notifyDataSetChanged(); + } + } + + @Override + public void onLoadEntriesCompleted() { + rebuild(); + } + + @Override + public void onAllSizesComputed() { + } + + @Override + public void onLauncherInfoChanged() { + } + + @Override + public void onPackageIconChanged() { + } + + @Override + public void onPackageSizeChanged(String packageName) { + } + + @Override + public void onRunningStateChanged(boolean running) { + } + + private void handleAppEntries(List entries) { + final ArrayList sections = new ArrayList(); + final ArrayList positions = new ArrayList(); + final PackageManager pm = getActivity().getPackageManager(); + String lastSectionIndex = null; + int offset = 0; + + for (int i = 0; i < entries.size(); i++) { + final ApplicationInfo info = entries.get(i).info; + final String label = (String) info.loadLabel(pm); + final String sectionIndex; + + if (!info.enabled) { + sectionIndex = "--"; // XXX + } else if (TextUtils.isEmpty(label)) { + sectionIndex = ""; + } else { + sectionIndex = label.substring(0, 1).toUpperCase(); + } + + if (lastSectionIndex == null || + !TextUtils.equals(sectionIndex, lastSectionIndex)) { + sections.add(sectionIndex); + positions.add(offset); + lastSectionIndex = sectionIndex; + } + + offset++; + } + + mAllPackagesAdapter.setEntries(entries, sections, positions); + mEntryMap.clear(); + for (ApplicationsState.AppEntry e : entries) { + mEntryMap.put(e.info.packageName, e); + } + } + + private void rebuild() { + mSession.rebuild(mActivityFilter, ApplicationsState.ALPHA_COMPARATOR); + } + + private int getStateDrawable(int state) { + switch (state) { + case ThermalUtils.STATE_BENCHMARK: + return R.drawable.ic_thermal_benchmark; + case ThermalUtils.STATE_BROWSER: + return R.drawable.ic_thermal_browser; + case ThermalUtils.STATE_CAMERA: + return R.drawable.ic_thermal_camera; + case ThermalUtils.STATE_DIALER: + return R.drawable.ic_thermal_dialer; + case ThermalUtils.STATE_GAMING: + return R.drawable.ic_thermal_gaming; + case ThermalUtils.STATE_STREAMING: + return R.drawable.ic_thermal_streaming; + case ThermalUtils.STATE_DEFAULT: + default: + return R.drawable.ic_thermal_default; + } + } + + private class ViewHolder extends RecyclerView.ViewHolder { + private TextView title; + private Spinner mode; + private ImageView icon; + private View rootView; + private ImageView stateIcon; + + private ViewHolder(View view) { + super(view); + this.title = view.findViewById(R.id.app_name); + this.mode = view.findViewById(R.id.app_mode); + this.icon = view.findViewById(R.id.app_icon); + this.stateIcon = view.findViewById(R.id.state); + this.rootView = view; + + view.setTag(this); + } + } + + private class ModeAdapter extends BaseAdapter { + + private final LayoutInflater inflater; + private final int[] items = { + R.string.thermal_default, + R.string.thermal_benchmark, + R.string.thermal_browser, + R.string.thermal_camera, + R.string.thermal_dialer, + R.string.thermal_gaming, + R.string.thermal_streaming + }; + + private ModeAdapter(Context context) { + inflater = LayoutInflater.from(context); + } + + @Override + public int getCount() { + return items.length; + } + + @Override + public Object getItem(int position) { + return items[position]; + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView view; + if (convertView != null) { + view = (TextView) convertView; + } else { + view = (TextView) inflater.inflate(android.R.layout.simple_spinner_dropdown_item, + parent, false); + } + + view.setText(items[position]); + view.setTextSize(14f); + return view; + } + } + + private class AllPackagesAdapter extends RecyclerView.Adapter + implements AdapterView.OnItemSelectedListener, SectionIndexer { + + private List mEntries = new ArrayList<>(); + private String[] mSections; + private int[] mPositions; + + public AllPackagesAdapter(Context context) { + mActivityFilter = new ActivityFilter(context.getPackageManager()); + } + + @Override + public int getItemCount() { + return mEntries.size(); + } + + @Override + public long getItemId(int position) { + return mEntries.get(position).id; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.thermal_list_item, parent, false)); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Context context = holder.itemView.getContext(); + ApplicationsState.AppEntry entry = mEntries.get(position); + if (entry == null) { + return; + } + + holder.mode.setAdapter(new ModeAdapter(context)); + holder.mode.setOnItemSelectedListener(this); + holder.title.setText(entry.label); + holder.title.setOnClickListener(v -> holder.mode.performClick()); + mApplicationsState.ensureIcon(entry); + holder.icon.setImageDrawable(entry.icon); + int packageState = mThermalUtils.getStateForPackage(entry.info.packageName); + holder.mode.setSelection(packageState, false); + holder.mode.setTag(entry); + holder.stateIcon.setImageResource(getStateDrawable(packageState)); + } + + private void setEntries(List entries, + List sections, List positions) { + mEntries = entries; + mSections = sections.toArray(new String[sections.size()]); + mPositions = new int[positions.size()]; + for (int i = 0; i < positions.size(); i++) { + mPositions[i] = positions.get(i); + } + notifyDataSetChanged(); + } + + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + final ApplicationsState.AppEntry entry = (ApplicationsState.AppEntry) parent.getTag(); + int currentState = mThermalUtils.getStateForPackage(entry.info.packageName); + if (currentState != position) { + mThermalUtils.writePackage(entry.info.packageName, position); + notifyDataSetChanged(); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + + @Override + public int getPositionForSection(int section) { + if (section < 0 || section >= mSections.length) { + return -1; + } + + return mPositions[section]; + } + + @Override + public int getSectionForPosition(int position) { + if (position < 0 || position >= getItemCount()) { + return -1; + } + + final int index = Arrays.binarySearch(mPositions, position); + + /* + * Consider this example: section positions are 0, 3, 5; the supplied + * position is 4. The section corresponding to position 4 starts at + * position 3, so the expected return value is 1. Binary search will not + * find 4 in the array and thus will return -insertPosition-1, i.e. -3. + * To get from that number to the expected value of 1 we need to negate + * and subtract 2. + */ + return index >= 0 ? index : -index - 2; + } + + @Override + public Object[] getSections() { + return mSections; + } + } + + private class ActivityFilter implements ApplicationsState.AppFilter { + + private final PackageManager mPackageManager; + private final List mLauncherResolveInfoList = new ArrayList(); + + private ActivityFilter(PackageManager packageManager) { + this.mPackageManager = packageManager; + + updateLauncherInfoList(); + } + + public void updateLauncherInfoList() { + Intent i = new Intent(Intent.ACTION_MAIN); + i.addCategory(Intent.CATEGORY_LAUNCHER); + List resolveInfoList = mPackageManager.queryIntentActivities(i, 0); + + synchronized (mLauncherResolveInfoList) { + mLauncherResolveInfoList.clear(); + for (ResolveInfo ri : resolveInfoList) { + mLauncherResolveInfoList.add(ri.activityInfo.packageName); + } + } + } + + @Override + public void init() { + } + + @Override + public boolean filterApp(ApplicationsState.AppEntry entry) { + boolean show = !mAllPackagesAdapter.mEntries.contains(entry.info.packageName); + if (show) { + synchronized (mLauncherResolveInfoList) { + show = mLauncherResolveInfoList.contains(entry.info.packageName); + } + } + return show; + } + } +} diff --git a/parts/src/org/lineageos/settings/thermal/ThermalUtils.java b/parts/src/org/lineageos/settings/thermal/ThermalUtils.java new file mode 100644 index 0000000..fa538b0 --- /dev/null +++ b/parts/src/org/lineageos/settings/thermal/ThermalUtils.java @@ -0,0 +1,161 @@ +/* + * 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.thermal; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.UserHandle; + +import androidx.preference.PreferenceManager; + +import org.lineageos.settings.utils.FileUtils; + +public final class ThermalUtils { + + private static final String THERMAL_CONTROL = "thermal_control"; + + protected static final int STATE_DEFAULT = 0; + protected static final int STATE_BENCHMARK = 1; + protected static final int STATE_BROWSER = 2; + protected static final int STATE_CAMERA = 3; + protected static final int STATE_DIALER = 4; + protected static final int STATE_GAMING = 5; + protected static final int STATE_STREAMING = 6; + + private static final String THERMAL_STATE_DEFAULT = "0"; + private static final String THERMAL_STATE_BENCHMARK = "10"; + private static final String THERMAL_STATE_BROWSER = "11"; + private static final String THERMAL_STATE_CAMERA = "12"; + private static final String THERMAL_STATE_DIALER = "8"; + private static final String THERMAL_STATE_GAMING = "9"; + private static final String THERMAL_STATE_STREAMING = "14"; + + private static final String THERMAL_BENCHMARK = "thermal.benchmark="; + private static final String THERMAL_BROWSER = "thermal.browser="; + private static final String THERMAL_CAMERA = "thermal.camera="; + private static final String THERMAL_DIALER = "thermal.dialer="; + private static final String THERMAL_GAMING = "thermal.gaming="; + private static final String THERMAL_STREAMING = "thermal.streaming="; + + private static final String THERMAL_SCONFIG = "/sys/class/thermal/thermal_message/sconfig"; + + private SharedPreferences mSharedPrefs; + + protected ThermalUtils(Context context) { + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); + } + + public static void startService(Context context) { + context.startServiceAsUser(new Intent(context, ThermalService.class), + UserHandle.CURRENT); + } + + private void writeValue(String profiles) { + mSharedPrefs.edit().putString(THERMAL_CONTROL, profiles).apply(); + } + + private String getValue() { + String value = mSharedPrefs.getString(THERMAL_CONTROL, null); + + if (value == null || value.isEmpty()) { + value = THERMAL_BENCHMARK + ":" + THERMAL_BROWSER + ":" + THERMAL_CAMERA + ":" + + THERMAL_DIALER + ":" + THERMAL_GAMING + ":" + THERMAL_STREAMING; + writeValue(value); + } + return value; + } + + protected void writePackage(String packageName, int mode) { + String value = getValue(); + value = value.replace(packageName + ",", ""); + String[] modes = value.split(":"); + String finalString; + + switch (mode) { + case STATE_BENCHMARK: + modes[0] = modes[0] + packageName + ","; + break; + case STATE_BROWSER: + modes[1] = modes[1] + packageName + ","; + break; + case STATE_CAMERA: + modes[2] = modes[2] + packageName + ","; + break; + case STATE_DIALER: + modes[3] = modes[3] + packageName + ","; + break; + case STATE_GAMING: + modes[4] = modes[4] + packageName + ","; + break; + case STATE_STREAMING: + modes[5] = modes[5] + packageName + ","; + break; + } + + finalString = modes[0] + ":" + modes[1] + ":" + modes[2] + ":" + modes[3] + ":" + + modes[4] + ":" + modes[5]; + + writeValue(finalString); + } + + protected int getStateForPackage(String packageName) { + String value = getValue(); + String[] modes = value.split(":"); + int state = STATE_DEFAULT; + if (modes[0].contains(packageName + ",")) { + state = STATE_BENCHMARK; + } else if (modes[1].contains(packageName + ",")) { + state = STATE_BROWSER; + } else if (modes[2].contains(packageName + ",")) { + state = STATE_CAMERA; + } else if (modes[3].contains(packageName + ",")) { + state = STATE_DIALER; + } else if (modes[4].contains(packageName + ",")) { + state = STATE_GAMING; + } else if (modes[5].contains(packageName + ",")) { + state = STATE_STREAMING; + } + + return state; + } + + protected void setThermalProfile(String packageName) { + String value = getValue(); + String modes[]; + String state = THERMAL_STATE_DEFAULT; + + if (value != null) { + modes = value.split(":"); + + if (modes[0].contains(packageName + ",")) { + state = THERMAL_STATE_BENCHMARK; + } else if (modes[1].contains(packageName + ",")) { + state = THERMAL_STATE_BROWSER; + } else if (modes[2].contains(packageName + ",")) { + state = THERMAL_STATE_CAMERA; + } else if (modes[3].contains(packageName + ",")) { + state = THERMAL_STATE_DIALER; + } else if (modes[4].contains(packageName + ",")) { + state = THERMAL_STATE_GAMING; + } else if (modes[5].contains(packageName + ",")) { + state = THERMAL_STATE_STREAMING; + } + } + FileUtils.writeLine(THERMAL_SCONFIG, state); + } +} 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..00028ff --- /dev/null +++ b/parts/src/org/lineageos/settings/utils/FileUtils.java @@ -0,0 +1,160 @@ +/* + * 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; + } +} diff --git a/rootdir/etc/init.target.rc b/rootdir/etc/init.target.rc index 73a28ee..990acaa 100644 --- a/rootdir/etc/init.target.rc +++ b/rootdir/etc/init.target.rc @@ -152,7 +152,10 @@ on boot setprop vendor.usb.controller 4e00000.dwc3 #ExtR HONGMI-90116,wufa@wingtech.com,add,20210908,add mi_thermal + chmod 0664 /sys/class/thermal/thermal_message/sconfig chown system system /sys/class/thermal/thermal_message/sconfig + chmod 0666 /sys/class/thermal/thermal_message/temp_state + chown system system /sys/class/thermal/thermal_message/temp_state # add dual mkdir /mnt/vendor/persist/camera 0777 system system diff --git a/sepolicy/private/devicesettings_app.te b/sepolicy/private/devicesettings_app.te new file mode 100644 index 0000000..2f6a3d0 --- /dev/null +++ b/sepolicy/private/devicesettings_app.te @@ -0,0 +1,28 @@ +app_domain(devicesettings_app) + +# Allow devicesettings_app to find *_service +allow devicesettings_app { + app_api_service + audioserver_service + cameraserver_service + drmserver_service + mediaextractor_service + mediametrics_service + mediaserver_service +}:service_manager find; + + +# Allow devicesettings_app read and write /data/data subdirectory +allow devicesettings_app system_app_data_file:dir create_dir_perms; +allow devicesettings_app system_app_data_file:{ file lnk_file } create_file_perms; + +# Allow binder communication with gpuservice +binder_call(devicesettings_app, gpuservice) + +# Allow devicesettings_app to read and write to cgroup/sysfs_leds/sysfs_thermal +allow devicesettings_app sysfs_leds:dir search; +allow devicesettings_app { + cgroup + sysfs_leds + sysfs_thermal +}:{ file lnk_file } rw_file_perms; diff --git a/sepolicy/private/property.te b/sepolicy/private/property.te new file mode 100644 index 0000000..27d0385 --- /dev/null +++ b/sepolicy/private/property.te @@ -0,0 +1,2 @@ +# SettingsLib +system_public_prop(settingslib_prop) diff --git a/sepolicy/private/property_contexts b/sepolicy/private/property_contexts index 05840c9..459b39f 100644 --- a/sepolicy/private/property_contexts +++ b/sepolicy/private/property_contexts @@ -37,3 +37,9 @@ wifi.pktlog.debug.0.chen u:object_r:exported_system_prop:s0 # ro.factory_mode u:object_r:exported_default_prop:s0 + +# SettingsLib +settingsdebug.instant.packages u:object_r:settingslib_prop:s0 + +# Camera +vendor.camera.aux.packagelist u:object_r:vendor_camera_prop:s0 diff --git a/sepolicy/private/system_app.te b/sepolicy/private/system_app.te index 5fae7c8..c2e3218 100644 --- a/sepolicy/private/system_app.te +++ b/sepolicy/private/system_app.te @@ -5,3 +5,6 @@ allow system_app sysfs_zram:file r_file_perms; # Allow settings to query qemu.hw.mainkeys get_prop(system_app, qemu_hw_prop) + +# Allow System Apps to get settingsdebug.instant.packages prop +get_prop(system_app, settingslib_prop) diff --git a/sepolicy/public/devicesettings_app.te b/sepolicy/public/devicesettings_app.te new file mode 100644 index 0000000..9c05e95 --- /dev/null +++ b/sepolicy/public/devicesettings_app.te @@ -0,0 +1,2 @@ +type devicesettings_app, domain; +typeattribute devicesettings_app mlstrustedsubject; diff --git a/sepolicy/vendor/devicesettings_app.te b/sepolicy/vendor/devicesettings_app.te new file mode 100644 index 0000000..34aa534 --- /dev/null +++ b/sepolicy/vendor/devicesettings_app.te @@ -0,0 +1,9 @@ +allow devicesettings_app vendor_sysfs_graphics:dir search; +allow devicesettings_app vendor_sysfs_graphics:file rw_file_perms; + +allow devicesettings_app vendor_sysfs_kgsl:dir search; + +allow devicesettings_app vendor_sysfs_kgsl:{ file lnk_file } rw_file_perms; + +allow devicesettings_app vendor_sysfs_graphics:file write; +r_dir_file(devicesettings_app, vendor_sysfs_graphics) diff --git a/sepolicy/vendor/hal_fingerprint_default.te b/sepolicy/vendor/hal_fingerprint_default.te index 4704107..58c43a3 100644 --- a/sepolicy/vendor/hal_fingerprint_default.te +++ b/sepolicy/vendor/hal_fingerprint_default.te @@ -8,6 +8,9 @@ allow hal_fingerprint_default vendor_hal_perf_hwservice:hwservice_manager find; allow hal_fingerprint_default fingerprint_data_file:dir rw_dir_perms; allow hal_fingerprint_default fingerprint_data_file:file create_file_perms; +allow hal_fingerprint_default system_prop:property_service { set }; +allow hal_fingerprint_default exported_system_prop:property_service { set }; + # Dev nodes allow hal_fingerprint_default { fingerprint_device diff --git a/sepolicy/vendor/hal_sensors_default.te b/sepolicy/vendor/hal_sensors_default.te index 4b53b97..4960ff7 100644 --- a/sepolicy/vendor/hal_sensors_default.te +++ b/sepolicy/vendor/hal_sensors_default.te @@ -4,6 +4,8 @@ hal_client_domain(hal_sensors_default, hal_audio) allow hal_sensors_default hal_sensors_default:qipcrtr_socket { ioctl }; +allow hal_sensors_default sysfs:file { read write }; + allow hal_sensors_default audio_socket:sock_file rw_file_perms; allow hal_sensors_default socket_device:sock_file rw_file_perms; allow hal_sensors_default sound_device:chr_file rw_file_perms; diff --git a/sepolicy/vendor/stflashtool.te b/sepolicy/vendor/stflashtool.te index a5feae0..62a8c63 100644 --- a/sepolicy/vendor/stflashtool.te +++ b/sepolicy/vendor/stflashtool.te @@ -8,3 +8,5 @@ allow stflashtool nfc_device:chr_file {ioctl read write getattr lock append map get_prop(stflashtool, vendor_radio_prop) get_prop(stflashtool, vendor_nfc_prop) set_prop(stflashtool, vendor_nfc_prop) + +allow stflashtool nfc_prop:file { read }; diff --git a/sepolicy/vendor/system_app.te b/sepolicy/vendor/system_app.te index 4a4a48b..049faaa 100644 --- a/sepolicy/vendor/system_app.te +++ b/sepolicy/vendor/system_app.te @@ -18,3 +18,7 @@ allow system_app vendor_sysfs_battery_supply:dir { search }; allow system_app vendor_sysfs_battery_supply:file { getattr open read }; r_dir_file(system_app, vendor_sysfs_battery_supply) + +allow system_app sysfs:file { setattr getattr write open read }; +allow system_app vendor_sysfs_graphics:dir { search setattr getattr write open read }; +allow system_app privapp_data_file:dir { search }; diff --git a/sepolicy/vendor/system_server.te b/sepolicy/vendor/system_server.te index 0499c24..1ef38f2 100644 --- a/sepolicy/vendor/system_server.te +++ b/sepolicy/vendor/system_server.te @@ -10,6 +10,7 @@ allow system_server system_file:file r_file_perms; # Allow system_server to set vendor_persist_camera_prop get_prop(system_server, vendor_camera_prop) get_prop(system_server,vendor_persist_camera_prop) +allow system_server vendor_camera_prop:file { read open }; # Allow system_server to read Fast Charging status allow system_server vendor_sysfs_battery_supply:file { getattr open read }; diff --git a/sepolicy/vendor/untrusted_app.te b/sepolicy/vendor/untrusted_app.te index cd9be2a..9130c0a 100644 --- a/sepolicy/vendor/untrusted_app.te +++ b/sepolicy/vendor/untrusted_app.te @@ -4,3 +4,14 @@ allow untrusted_app proc_zoneinfo:file r_file_perms; allow untrusted_app tmpfs:lnk_file { read }; allow untrusted_app shell_test_data_file:dir { search }; allow untrusted_app app_data_file:file { execute }; + +allow untrusted_app mnt_vendor_file:dir { search }; +allow untrusted_app mnt_vendor_file:dir { getattr }; +allow untrusted_app block_device:dir { search }; +allow untrusted_app proc_overcommit_memory:file { read }; + +allow untrusted_app proc_tty_drivers:file { read }; +allow untrusted_app qemu_hw_prop:file { read }; +allow untrusted_app qemu_sf_lcd_density_prop:file { read }; +allow untrusted_app serialno_prop:file { read }; +allow untrusted_app proc_max_map_count:file { read }; diff --git a/sepolicy/vendor/vendor_hal_gnss_qti.te b/sepolicy/vendor/vendor_hal_gnss_qti.te new file mode 100644 index 0000000..eb2c9d0 --- /dev/null +++ b/sepolicy/vendor/vendor_hal_gnss_qti.te @@ -0,0 +1 @@ +allow vendor_hal_gnss_qti system_prop:file { read }; diff --git a/sepolicy/vendor/vendor_hvdcp.te b/sepolicy/vendor/vendor_hvdcp.te index 034fb14..69015ca 100644 --- a/sepolicy/vendor/vendor_hvdcp.te +++ b/sepolicy/vendor/vendor_hvdcp.te @@ -1 +1,3 @@ allow vendor_hvdcp kmsg_device:chr_file rw_file_perms; + +allow vendor_hvdcp vendor_sysfs_iio:dir { read }; diff --git a/sepolicy/vendor/vendor_qti_init_shell.te b/sepolicy/vendor/vendor_qti_init_shell.te index 9a61c4e..e972ffe 100644 --- a/sepolicy/vendor/vendor_qti_init_shell.te +++ b/sepolicy/vendor/vendor_qti_init_shell.te @@ -9,3 +9,5 @@ allow vendor_qti_init_shell sysfs_wakeup:file setattr; allow vendor_qti_init_shell sysfs:file { setattr write }; allow vendor_qti_init_shell proc_watermark_scale_factor:file w_file_perms; allow vendor_qti_init_shell proc_watermark_boost_factor:file w_file_perms; + +allow vendor_qti_init_shell vendor_sysfs_qdss_dev:file { setattr };