feat: SettingsLib验证通过-使用App module启用

This commit is contained in:
2024-12-09 16:04:49 +08:00
parent 1f18a59dab
commit a7f5c61005
1562 changed files with 181632 additions and 18 deletions

View File

@@ -0,0 +1,149 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings;
import static android.car.CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
import android.annotation.Nullable;
import android.app.Application;
import android.car.Car;
import android.car.Car.CarServiceLifecycleListener;
import android.car.CarOccupantZoneManager;
import android.car.CarOccupantZoneManager.OccupantZoneConfigChangeListener;
import android.car.CarOccupantZoneManager.OccupantZoneInfo;
import android.car.media.CarAudioManager;
import android.view.Display;
import androidx.annotation.GuardedBy;
/**
* Application class for CarSettings.
*/
public class CarSettingsApplication extends Application {
private CarOccupantZoneManager mCarOccupantZoneManager;
private final Object mInfoLock = new Object();
private final Object mCarAudioManagerLock = new Object();
@GuardedBy("mInfoLock")
private int mOccupantZoneDisplayId = Display.DEFAULT_DISPLAY;
@GuardedBy("mInfoLock")
private int mAudioZoneId = CarAudioManager.INVALID_AUDIO_ZONE;
@GuardedBy("mInfoLock")
private int mOccupantZoneType = CarOccupantZoneManager.OCCUPANT_TYPE_INVALID;
@GuardedBy("mCarAudioManagerLock")
private CarAudioManager mCarAudioManager = null;
/**
* Listener to monitor any Occupant Zone configuration change.
*/
private final OccupantZoneConfigChangeListener mConfigChangeListener = flags -> {
synchronized (mInfoLock) {
updateZoneInfoLocked();
}
};
/**
* Listener to monitor the Lifecycle of car service.
*/
private final CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
if (!ready) {
mCarOccupantZoneManager = null;
synchronized (mCarAudioManagerLock) {
mCarAudioManager = null;
}
return;
}
mCarOccupantZoneManager = (CarOccupantZoneManager) car.getCarManager(
Car.CAR_OCCUPANT_ZONE_SERVICE);
if (mCarOccupantZoneManager != null) {
mCarOccupantZoneManager.registerOccupantZoneConfigChangeListener(
mConfigChangeListener);
}
synchronized (mCarAudioManagerLock) {
mCarAudioManager = (CarAudioManager) car.getCarManager(Car.AUDIO_SERVICE);
}
synchronized (mInfoLock) {
updateZoneInfoLocked();
}
};
@Override
public void onCreate() {
super.onCreate();
Car.createCar(this, /* handler= */ null , Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
mCarServiceLifecycleListener);
}
/**
* Returns zone type assigned for the current user.
* The zone type is used to determine whether the settings preferences
* should be available or not.
*/
public final int getMyOccupantZoneType() {
synchronized (mInfoLock) {
return mOccupantZoneType;
}
}
/**
* Returns displayId assigned for the current user.
*/
public final int getMyOccupantZoneDisplayId() {
synchronized (mInfoLock) {
return mOccupantZoneDisplayId;
}
}
/**
* Returns audio zone id assigned for the current user.
*/
public final int getMyAudioZoneId() {
synchronized (mInfoLock) {
return mAudioZoneId;
}
}
/**
* Returns CarAudioManager instance.
*/
@Nullable
public final CarAudioManager getCarAudioManager() {
synchronized (mCarAudioManagerLock) {
return mCarAudioManager;
}
}
@GuardedBy("mInfoLock")
private void updateZoneInfoLocked() {
if (mCarOccupantZoneManager == null) {
return;
}
OccupantZoneInfo info = mCarOccupantZoneManager.getMyOccupantZone();
if (info != null) {
mOccupantZoneType = info.occupantType;
mAudioZoneId = mCarOccupantZoneManager.getAudioZoneIdForOccupant(info);
Display display = mCarOccupantZoneManager
.getDisplayForOccupant(info, DISPLAY_TYPE_MAIN);
if (display != null) {
mOccupantZoneDisplayId = display.getDisplayId();
}
}
}
}

View File

@@ -0,0 +1,191 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings;
import static android.car.settings.CarSettings.Global.ENABLE_USER_SWITCH_DEVELOPER_MESSAGE;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.SystemClock;
import android.os.UserManager;
import android.provider.Settings;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
import android.view.animation.AnimationUtils;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.car.internal.common.UserHelperLite;
import com.android.car.settings.common.Logger;
import java.util.Objects;
/**
* Copied over from phone settings. This covers the fallback case where no launcher is available.
*/
public class FallbackHome extends Activity {
private static final Logger LOG = new Logger(FallbackHome.class);
private static final int PROGRESS_TIMEOUT = 2000;
private boolean mProvisioned;
private boolean mFinished;
private final Runnable mProgressTimeoutRunnable = () -> {
View v = getLayoutInflater().inflate(
R.layout.fallback_home_finishing_boot, /* root= */ null);
setContentView(v);
v.setAlpha(0f);
v.animate()
.alpha(1f)
.setDuration(500)
.setInterpolator(AnimationUtils.loadInterpolator(
this, android.R.interpolator.fast_out_slow_in))
.start();
getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set ourselves totally black before the device is provisioned
mProvisioned = Settings.Global.getInt(getContentResolver(),
Settings.Global.DEVICE_PROVISIONED, 0) != 0;
int flags;
boolean showInfo = false;
if (!mProvisioned) {
setTheme(R.style.FallbackHome_SetupWizard);
flags = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
} else {
flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
showInfo = "true".equals(Settings.Global.getString(getContentResolver(),
ENABLE_USER_SWITCH_DEVELOPER_MESSAGE));
}
if (showInfo) {
// Display some info about the current user, which is useful to debug / track user
// switching delays.
// NOTE: we're manually creating the view (instead of inflating it from XML) to
// minimize the performance impact.
TextView view = new TextView(this);
view.setText("FallbackHome for user " + getUserId() + ".\n\n"
+ "This activity is displayed while the user is starting, \n"
+ "and it will be replaced by the proper Home \n"
+ "(once the user is unlocked).\n\n"
+ "NOTE: this message is only shown on debuggable builds");
view.setGravity(Gravity.CENTER);
view.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
LinearLayout parent = new LinearLayout(this);
parent.setOrientation(LinearLayout.VERTICAL);
parent.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
parent.addView(view);
setContentView(parent);
}
getWindow().getDecorView().setSystemUiVisibility(flags);
registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
maybeFinish();
}
@Override
protected void onResume() {
super.onResume();
LOG.d("onResume() for user " + getUserId() + ". Provisioned: " + mProvisioned);
if (mProvisioned) {
mHandler.postDelayed(mProgressTimeoutRunnable, PROGRESS_TIMEOUT);
}
}
@Override
protected void onPause() {
super.onPause();
mHandler.removeCallbacks(mProgressTimeoutRunnable);
}
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mReceiver);
if (!mFinished) {
LOG.d("User " + getUserId() + " FallbackHome is finished");
finishFallbackHome();
}
}
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
maybeFinish();
}
};
private void maybeFinish() {
UserManager userManager = getSystemService(UserManager.class);
if (userManager.isUserUnlocked()) {
final Intent homeIntent = new Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_HOME);
final ResolveInfo homeInfo = getPackageManager().resolveActivity(homeIntent, 0);
if (Objects.equals(getPackageName(), homeInfo.activityInfo.packageName)) {
LOG.d("User " + getUserId() + " unlocked but no home; let's hope someone enables "
+ "one soon?");
mHandler.sendEmptyMessageDelayed(0, 500);
} else {
String homePackageName = homeInfo.activityInfo.packageName;
boolean isMultiUserNoDriver =
userManager.isVisibleBackgroundUsersOnDefaultDisplaySupported();
if (UserHelperLite.isHeadlessSystemUser(getUserId()) && !isMultiUserNoDriver) {
// This is the transient state in HeadlessSystemMode to boot for user 10+.
LOG.d("User 0 unlocked, but will not launch real home: " + homePackageName);
return;
}
LOG.d("User " + getUserId() + " unlocked and real home (" + homePackageName
+ ") found; let's go!");
finishFallbackHome();
}
}
}
private void finishFallbackHome() {
getSystemService(PowerManager.class).userActivity(SystemClock.uptimeMillis(), false);
finishAndRemoveTask();
mFinished = true;
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
maybeFinish();
}
};
}

View File

@@ -0,0 +1,12 @@
{
"auto-end-to-end-postsubmit": [
{
"name": "AndroidAutomotiveSettingsTests",
"options" : [
{
"include-filter": "android.platform.tests.SettingTest"
}
]
}
]
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import android.provider.Settings;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
import com.android.car.settings.search.CarBaseSearchIndexProvider;
import com.android.settingslib.search.SearchIndexable;
/**
* The main page for accessibility settings. This allows users to change accessibility settings
* like closed captions styling and text size.
*/
@SearchIndexable
public class AccessibilitySettingsFragment extends SettingsFragment {
@Override
protected int getPreferenceScreenResId() {
return R.xml.accessibility_settings_fragment;
}
public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new CarBaseSearchIndexProvider(R.xml.accessibility_settings_fragment,
Settings.ACTION_ACCESSIBILITY_SETTINGS);
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.provider.Settings;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* {@link PreferenceController} for the preference which leads the user to the caption settings
* from within accessibility settings.
*/
public class CaptionSettingsPreferenceController extends
PreferenceController<Preference> {
public CaptionSettingsPreferenceController(Context context,
String preferenceKey, FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected void updateState(Preference preference) {
preference.setSummary(getSummary());
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
private CharSequence getSummary() {
boolean captionsEnabled = Settings.Secure.getInt(getContext().getContentResolver(),
Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, 0) != 0;
return captionsEnabled ? getContext().getString(R.string.captions_settings_on)
: getContext().getString(R.string.captions_settings_off);
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
/**
* Fragment for video closed caption related settings.
*/
public class CaptionsSettingsFragment extends SettingsFragment {
@Override
protected int getPreferenceScreenResId() {
return R.xml.captions_settings_fragment;
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.provider.Settings;
import androidx.annotation.VisibleForTesting;
import androidx.preference.ListPreference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* Preference controller for setting the captions text size. This is achieved through the settings
* secure constant {@link Settings.Secure#ACCESSIBILITY_CAPTIONING_FONT_SCALE}.
*/
public class CaptionsTextSizeListPreferenceController extends PreferenceController<ListPreference> {
private static final int DEFAULT_SELECTOR_INDEX = 2;
private static final float DEFAULT_TEXT_SIZE = 1.0F;
@VisibleForTesting
final String[] mFontSizeTitles;
private final String[] mFontSizeStringValues;
private final float[] mFontSizeFloatValues;
public CaptionsTextSizeListPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mFontSizeTitles = new String[]{
context.getString(R.string.captions_settings_text_size_very_small),
context.getString(R.string.captions_settings_text_size_small),
context.getString(R.string.captions_settings_text_size_default),
context.getString(R.string.captions_settings_text_size_large),
context.getString(R.string.captions_settings_text_size_very_large)
};
mFontSizeStringValues = new String[]{
"0.25",
"0.5",
"1.0",
"1.5",
"2.0"
};
mFontSizeFloatValues = new float[mFontSizeStringValues.length];
for (int i = 0; i < mFontSizeStringValues.length; i++) {
mFontSizeFloatValues[i] = Float.parseFloat(mFontSizeStringValues[i]);
}
}
@Override
protected Class<ListPreference> getPreferenceType() {
return ListPreference.class;
}
@Override
protected void updateState(ListPreference preference) {
preference.setEntries(mFontSizeTitles);
preference.setEntryValues(mFontSizeStringValues);
int currentFontSizeIndex = getCurrentSelectedFontSizeIndex();
preference.setValueIndex(currentFontSizeIndex);
preference.setSummary(getSummary(currentFontSizeIndex));
}
@Override
public boolean handlePreferenceChanged(ListPreference preference, Object newValue) {
float newFontValue = Float.parseFloat((String) newValue);
Settings.Secure.putFloat(getContext().getContentResolver(),
Settings.Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, newFontValue);
return true;
}
private CharSequence getSummary(int currentFontSizeIndex) {
return mFontSizeTitles[currentFontSizeIndex];
}
private int getCurrentSelectedFontSizeIndex() {
float currentFontScale = Settings.Secure.getFloat(getContext().getContentResolver(),
Settings.Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, DEFAULT_TEXT_SIZE);
int selectorIndex = DEFAULT_SELECTOR_INDEX;
for (int i = 0; i < mFontSizeFloatValues.length; i++) {
if (mFontSizeFloatValues[i] == currentFontScale) {
selectorIndex = i;
break;
}
}
return selectorIndex;
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.provider.Settings;
import androidx.annotation.VisibleForTesting;
import androidx.preference.ListPreference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* Preference controller for setting the captions text style. This is achieved through the settings
* secure constant {@link Settings.Secure#ACCESSIBILITY_CAPTIONING_PRESET}.
*/
public class CaptionsTextStyleListPreferenceController extends
PreferenceController<ListPreference> {
private static final int DEFAULT_SELECTOR_INDEX = 0;
private static final int DEFAULT_STYLE_PRESET = 4;
@VisibleForTesting
final String[] mFontStyleTitles;
private final String[] mFontStyleStringValues;
private final int[] mFontStyleIntValues;
public CaptionsTextStyleListPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mFontStyleTitles = new String[]{
context.getString(R.string.captions_settings_text_style_by_app),
context.getString(R.string.captions_settings_text_style_white_on_black),
context.getString(R.string.captions_settings_text_style_black_on_white),
context.getString(R.string.captions_settings_text_style_yellow_on_black),
context.getString(R.string.captions_settings_text_style_yellow_on_blue)
};
mFontStyleStringValues = new String[]{
"4",
"0",
"1",
"2",
"3"
};
mFontStyleIntValues = new int[mFontStyleStringValues.length];
for (int i = 0; i < mFontStyleStringValues.length; i++) {
mFontStyleIntValues[i] = Integer.parseInt(mFontStyleStringValues[i]);
}
}
@Override
protected Class<ListPreference> getPreferenceType() {
return ListPreference.class;
}
@Override
protected void updateState(ListPreference preference) {
preference.setEntries(mFontStyleTitles);
preference.setEntryValues(mFontStyleStringValues);
int currentFontStyleIndex = getCurrentSelectedFontStyleIndex();
preference.setValueIndex(currentFontStyleIndex);
preference.setSummary(getSummary(currentFontStyleIndex));
}
@Override
public boolean handlePreferenceChanged(ListPreference preference, Object newValue) {
int newFontValue = Integer.parseInt((String) newValue);
Settings.Secure.putInt(getContext().getContentResolver(),
Settings.Secure.ACCESSIBILITY_CAPTIONING_PRESET, newFontValue);
return true;
}
private CharSequence getSummary(int currentFontStyleIndex) {
return mFontStyleTitles[currentFontStyleIndex];
}
private int getCurrentSelectedFontStyleIndex() {
int currentFontStyle = Settings.Secure.getInt(getContext().getContentResolver(),
Settings.Secure.ACCESSIBILITY_CAPTIONING_PRESET, DEFAULT_STYLE_PRESET);
int selectorIndex = DEFAULT_SELECTOR_INDEX;
for (int i = 0; i < mFontStyleIntValues.length; i++) {
if (mFontStyleIntValues[i] == currentFontStyle) {
selectorIndex = i;
break;
}
}
return selectorIndex;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import androidx.preference.PreferenceGroup;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* Controller that ensures screen reader category is only shown when there are actionable items.
*/
public class ScreenReaderCategoryPreferenceController extends
PreferenceController<PreferenceGroup> {
public ScreenReaderCategoryPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<PreferenceGroup> getPreferenceType() {
return PreferenceGroup.class;
}
@Override
protected int getDefaultAvailabilityStatus() {
boolean screenReaderInstalled = ScreenReaderUtils.isScreenReaderInstalled(getContext());
return screenReaderInstalled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import com.android.car.settings.R;
import com.android.car.settings.common.ColoredSwitchPreference;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* Switch for enabling the default screen reader service.
*/
public class ScreenReaderEnabledSwitchPreferenceController extends
PreferenceController<ColoredSwitchPreference> {
public ScreenReaderEnabledSwitchPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<ColoredSwitchPreference> getPreferenceType() {
return ColoredSwitchPreference.class;
}
@Override
protected void updateState(ColoredSwitchPreference preference) {
getPreference().setTitle(getContext().getString(R.string.enable_screen_reader_toggle_title,
ScreenReaderUtils.getScreenReaderName(getContext())));
getPreference().setChecked(ScreenReaderUtils.isScreenReaderEnabled(getContext()));
}
@Override
protected boolean handlePreferenceChanged(ColoredSwitchPreference preference,
Object newValue) {
boolean enableScreenReader = (Boolean) newValue;
ScreenReaderUtils.setScreenReaderEnabled(getContext(), enableScreenReader);
return true;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
import com.android.car.ui.toolbar.ToolbarController;
/**
* Settings fragment for system screen reader.
*/
public class ScreenReaderSettingsFragment extends SettingsFragment {
@Override
protected int getPreferenceScreenResId() {
return R.xml.screen_reader_settings_fragment;
}
@Override
protected void setupToolbar(ToolbarController toolbar) {
super.setupToolbar(toolbar);
toolbar.setTitle(ScreenReaderUtils.getScreenReaderName(getContext()));
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import androidx.preference.Preference;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* Preference controller that intents out to the settings activity for the system default screen
* reader if it exists.
*/
public class ScreenReaderSettingsIntentPreferenceController extends
PreferenceController<Preference> {
public ScreenReaderSettingsIntentPreferenceController(Context context,
String preferenceKey,
FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
@Override
protected boolean handlePreferenceClicked(Preference preference) {
ComponentName screenReaderSettingsActivity =
ScreenReaderUtils.getScreenReaderSettingsActivity(getContext());
if (screenReaderSettingsActivity == null) {
return true;
}
Intent intent = new Intent(Intent.ACTION_MAIN).setComponent(
screenReaderSettingsActivity);
getContext().startActivity(intent);
return true;
}
@Override
protected int getDefaultAvailabilityStatus() {
ComponentName screenReaderSettingsActivity =
ScreenReaderUtils.getScreenReaderSettingsActivity(getContext());
return screenReaderSettingsActivity != null ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* Preference controller for the screen reader settings item. This updates the subtitle based on the
* enabled state of the system screen reader. The visibility is controlled by the parent {@link
* ScreenReaderCategoryPreferenceController}.
*/
public class ScreenReaderSettingsPreferenceController extends
PreferenceController<Preference> {
public ScreenReaderSettingsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
@Override
protected void updateState(Preference preference) {
preference.setTitle(ScreenReaderUtils.getScreenReaderName(getContext()));
preference.setSummary(getSummary());
}
private CharSequence getSummary() {
return ScreenReaderUtils.isScreenReaderEnabled(getContext())
? getContext().getString(R.string.screen_reader_settings_on)
: getContext().getString(R.string.screen_reader_settings_off);
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import androidx.preference.Preference;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* Preference controller for the subheader that provides the description of the screen reader
* service. This is pulled from the screen reader service description itself.
*/
public class ScreenReaderSettingsSubheaderPreferenceController extends
PreferenceController<Preference> {
public ScreenReaderSettingsSubheaderPreferenceController(Context context,
String preferenceKey,
FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
@Override
protected void updateState(Preference preference) {
preference.setSummary(ScreenReaderUtils.getScreenReaderDescription(getContext()));
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ServiceInfo;
import android.os.UserHandle;
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.Nullable;
import com.android.car.settings.R;
import com.android.internal.accessibility.util.AccessibilityUtils;
import java.util.List;
/**
* Hosts utility methods and constants for screen reader related settings.
*/
public class ScreenReaderUtils {
/**
* Returns whether the system screen reader is installed on the device.
*/
static boolean isScreenReaderInstalled(Context context) {
return getScreenReaderServiceInfo(context) != null;
}
/**
* Returns whether the default screen reader is currently enabled on the device.
*/
static boolean isScreenReaderEnabled(Context context) {
ComponentName screenReaderComponent = getScreenReaderComponentName(context);
if (screenReaderComponent == null) {
return false;
}
return AccessibilityUtils.getEnabledServicesFromSettings(context,
UserHandle.myUserId()).contains(screenReaderComponent);
}
/**
* Returns the name of the system default screen reader if it is installed. Returns an empty
* string if not.
*/
@Nullable
static CharSequence getScreenReaderName(Context context) {
AccessibilityServiceInfo serviceInfo = getScreenReaderServiceInfo(context);
if (serviceInfo == null) {
return "";
}
return serviceInfo.getResolveInfo().loadLabel(context.getPackageManager());
}
/**
* Returns the description of the system default screen reader if it is installed. Returns an
* empty string if not.
*/
static CharSequence getScreenReaderDescription(Context context) {
AccessibilityServiceInfo serviceInfo = getScreenReaderServiceInfo(context);
if (serviceInfo == null) {
return "";
}
String description = serviceInfo.loadDescription(context.getPackageManager());
return description != null ? description : "";
}
/**
* Returns the settings activity ComponentName of the system default screen reader if the screen
* reader is installed and a settings activity exists. Returns {@code null} otherwise.
*/
@Nullable
static ComponentName getScreenReaderSettingsActivity(Context context) {
AccessibilityServiceInfo serviceInfo = getScreenReaderServiceInfo(context);
if (serviceInfo == null) {
return null;
}
String settingsActivity = serviceInfo.getSettingsActivityName();
if (settingsActivity == null || settingsActivity.isEmpty()) {
return null;
}
return new ComponentName(getScreenReaderComponentName(context).getPackageName(),
settingsActivity);
}
/**
* Sets the screen reader enabled state. This should only be called if the screen reader is
* installed.
*/
static void setScreenReaderEnabled(Context context, boolean enabled) {
ComponentName screenReaderComponent = getScreenReaderComponentName(context);
if (screenReaderComponent == null) {
return;
}
AccessibilityUtils.setAccessibilityServiceState(context,
getScreenReaderComponentName(context), enabled,
UserHandle.myUserId());
}
private static ComponentName getScreenReaderComponentName(Context context) {
return ComponentName.unflattenFromString(
context.getString(R.string.config_default_screen_reader));
}
@Nullable
private static AccessibilityServiceInfo getScreenReaderServiceInfo(Context context) {
AccessibilityManager accessibilityManager = context.getSystemService(
AccessibilityManager.class);
if (accessibilityManager == null) {
return null;
}
List<AccessibilityServiceInfo> accessibilityServices =
accessibilityManager.getInstalledAccessibilityServiceList();
if (accessibilityServices == null) {
return null;
}
ComponentName screenReaderComponent = getScreenReaderComponentName(context);
if (screenReaderComponent == null) {
return null;
}
for (AccessibilityServiceInfo info : accessibilityServices) {
ServiceInfo serviceInfo = info.getResolveInfo().serviceInfo;
if (screenReaderComponent.getPackageName().equals(serviceInfo.packageName)
&& screenReaderComponent.getClassName().equals(serviceInfo.name)) {
return info;
}
}
return null;
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accessibility;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.provider.Settings;
import com.android.car.settings.common.ColoredSwitchPreference;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* Enables/disables default captions settings for video apps via the relevant settings constant.
*/
public class ShowCaptionsSwitchPreferenceController extends
PreferenceController<ColoredSwitchPreference> {
public ShowCaptionsSwitchPreferenceController(Context context,
String preferenceKey,
FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected void updateState(ColoredSwitchPreference preference) {
getPreference().setChecked(Settings.Secure.getInt(getContext().getContentResolver(),
Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, 0) != 0);
}
@Override
protected boolean handlePreferenceChanged(ColoredSwitchPreference preference,
Object newValue) {
boolean captionsEnabled = (Boolean) newValue;
Settings.Secure.putInt(getContext().getContentResolver(),
Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, captionsEnabled ? 1 : 0);
return true;
}
@Override
protected Class<ColoredSwitchPreference> getPreferenceType() {
return ColoredSwitchPreference.class;
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ContentResolver;
import android.content.Context;
import android.os.UserHandle;
import androidx.annotation.VisibleForTesting;
import androidx.preference.TwoStatePreference;
import com.android.car.settings.R;
import com.android.car.settings.common.ConfirmationDialogFragment;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* Controller for the preference that allows the user to toggle automatic syncing of accounts.
*
* <p>Copied from {@link com.android.settings.users.AutoSyncDataPreferenceController}
*/
public class AccountAutoSyncPreferenceController extends PreferenceController<TwoStatePreference> {
private final UserHandle mUserHandle;
/**
* Argument key to store a value that indicates whether the account auto sync is being enabled
* or disabled.
*/
@VisibleForTesting
static final String KEY_ENABLING = "ENABLING";
/** Argument key to store user handle. */
@VisibleForTesting
static final String KEY_USER_HANDLE = "USER_HANDLE";
@VisibleForTesting
final ConfirmationDialogFragment.ConfirmListener mConfirmListener = arguments -> {
boolean enabling = arguments.getBoolean(KEY_ENABLING);
UserHandle userHandle = arguments.getParcelable(KEY_USER_HANDLE);
ContentResolver.setMasterSyncAutomaticallyAsUser(enabling, userHandle.getIdentifier());
getPreference().setChecked(enabling);
};
public AccountAutoSyncPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mUserHandle = UserHandle.of(UserHandle.myUserId());
}
@Override
protected Class<TwoStatePreference> getPreferenceType() {
return TwoStatePreference.class;
}
@Override
protected void updateState(TwoStatePreference preference) {
preference.setChecked(ContentResolver.getMasterSyncAutomaticallyAsUser(
mUserHandle.getIdentifier()));
}
@Override
protected void onCreateInternal() {
// If the dialog is still up, reattach the preference
ConfirmationDialogFragment dialog =
(ConfirmationDialogFragment) getFragmentController().findDialogByTag(
ConfirmationDialogFragment.TAG);
ConfirmationDialogFragment.resetListeners(
dialog,
mConfirmListener,
/* rejectListener= */ null,
/* neutralListener= */ null);
}
@Override
protected boolean handlePreferenceChanged(TwoStatePreference preference, Object checked) {
getFragmentController().showDialog(
getAutoSyncChangeConfirmationDialogFragment((boolean) checked),
ConfirmationDialogFragment.TAG);
// The dialog will change the state of the preference if the user confirms, so don't handle
// it here
return false;
}
private ConfirmationDialogFragment getAutoSyncChangeConfirmationDialogFragment(
boolean enabling) {
int dialogTitle;
int dialogMessage;
if (enabling) {
dialogTitle = R.string.data_usage_auto_sync_on_dialog_title;
dialogMessage = R.string.data_usage_auto_sync_on_dialog;
} else {
dialogTitle = R.string.data_usage_auto_sync_off_dialog_title;
dialogMessage = R.string.data_usage_auto_sync_off_dialog;
}
ConfirmationDialogFragment dialogFragment =
new ConfirmationDialogFragment.Builder(getContext())
.setTitle(dialogTitle).setMessage(dialogMessage)
.setPositiveButton(R.string.allow, mConfirmListener)
.setNegativeButton(R.string.do_not_allow, /* rejectListener= */ null)
.addArgumentBoolean(KEY_ENABLING, enabling)
.addArgumentParcelable(KEY_USER_HANDLE, mUserHandle)
.build();
return dialogFragment;
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import android.accounts.Account;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.os.UserHandle;
import androidx.annotation.CallSuper;
import androidx.annotation.VisibleForTesting;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.PreferenceController;
import com.android.car.ui.preference.CarUiTwoActionIconPreference;
import com.android.settingslib.accounts.AuthenticatorHelper;
/** Base controller for preferences that shows the details of an account. */
public abstract class AccountDetailsBasePreferenceController
extends PreferenceController<CarUiTwoActionIconPreference> {
private static final Logger LOG = new Logger(AccountDetailsBasePreferenceController.class);
private Account mAccount;
private UserHandle mUserHandle;
public AccountDetailsBasePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
/** Sets the account that the details are shown for. */
public void setAccount(Account account) {
mAccount = account;
}
/** Returns the account the details are shown for. */
public Account getAccount() {
return mAccount;
}
/** Sets the UserHandle used by the controller. */
public void setUserHandle(UserHandle userHandle) {
mUserHandle = userHandle;
}
/** Returns the UserHandle used by the controller. */
public UserHandle getUserHandle() {
return mUserHandle;
}
@Override
protected Class<CarUiTwoActionIconPreference> getPreferenceType() {
return CarUiTwoActionIconPreference.class;
}
/**
* Verifies that the controller was properly initialized with
* {@link #setAccount(Account)} and {@link #setUserHandle(UserHandle)}.
*
* @throws IllegalStateException if the account or user handle are {@code null}
*/
@Override
@CallSuper
protected void checkInitialized() {
LOG.v("checkInitialized");
if (mAccount == null) {
throw new IllegalStateException(
"AccountDetailsBasePreferenceController must be initialized by calling "
+ "setAccount(Account)");
}
if (mUserHandle == null) {
throw new IllegalStateException(
"AccountDetailsBasePreferenceController must be initialized by calling "
+ "setUserHandle(UserHandle)");
}
}
@Override
@CallSuper
protected void updateState(CarUiTwoActionIconPreference preference) {
preference.setTitle(mAccount.name);
// Get the icon corresponding to the account's type and set it.
AuthenticatorHelper helper = getAuthenticatorHelper();
preference.setIcon(helper.getDrawableForType(getContext(), mAccount.type));
}
@VisibleForTesting
AuthenticatorHelper getAuthenticatorHelper() {
return new AuthenticatorHelper(getContext(), mUserHandle, null);
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.car.settings.accounts;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.content.pm.UserInfo;
import android.os.Bundle;
import android.os.UserHandle;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.XmlRes;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
import com.android.car.ui.toolbar.ToolbarController;
import com.android.settingslib.accounts.AuthenticatorHelper;
import java.util.Arrays;
/**
* Shows account details, and delete account option.
*/
public class AccountDetailsFragment extends SettingsFragment implements
AuthenticatorHelper.OnAccountsUpdateListener {
public static final String EXTRA_ACCOUNT = "extra_account";
public static final String EXTRA_ACCOUNT_LABEL = "extra_account_label";
public static final String EXTRA_USER_INFO = "extra_user_info";
private Account mAccount;
private UserInfo mUserInfo;
private AuthenticatorHelper mAuthenticatorHelper;
/**
* Creates a new AccountDetailsFragment.
*
* <p>Passes the provided account, label, and user info to the fragment via fragment arguments.
*/
public static AccountDetailsFragment newInstance(Account account, CharSequence label,
UserInfo userInfo) {
AccountDetailsFragment
accountDetailsFragment = new AccountDetailsFragment();
Bundle bundle = new Bundle();
bundle.putParcelable(EXTRA_ACCOUNT, account);
bundle.putCharSequence(EXTRA_ACCOUNT_LABEL, label);
bundle.putParcelable(EXTRA_USER_INFO, userInfo);
accountDetailsFragment.setArguments(bundle);
return accountDetailsFragment;
}
@Override
@XmlRes
protected int getPreferenceScreenResId() {
return R.xml.account_details_fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mAccount = getArguments().getParcelable(EXTRA_ACCOUNT);
mUserInfo = getArguments().getParcelable(EXTRA_USER_INFO);
use(AccountDetailsPreferenceController.class, R.string.pk_account_details)
.setAccount(mAccount);
use(AccountDetailsPreferenceController.class, R.string.pk_account_details)
.setUserHandle(mUserInfo.getUserHandle());
use(AccountSyncPreferenceController.class, R.string.pk_account_sync)
.setAccount(mAccount);
use(AccountDetailsSettingController.class, R.string.pk_account_settings)
.setAccount(mAccount);
use(AccountSyncPreferenceController.class, R.string.pk_account_sync)
.setUserHandle(mUserInfo.getUserHandle());
}
@Override
protected void setupToolbar(@NonNull ToolbarController toolbar) {
super.setupToolbar(toolbar);
// Set the fragment's title
toolbar.setTitle(getArguments().getCharSequence(EXTRA_ACCOUNT_LABEL));
}
@Override
public void onStart() {
super.onStart();
mAuthenticatorHelper = new AuthenticatorHelper(getContext(), mUserInfo.getUserHandle(),
this);
mAuthenticatorHelper.listenToAccountUpdates();
}
@Override
public void onStop() {
super.onStop();
mAuthenticatorHelper.stopListeningToAccountUpdates();
}
@Override
public void onAccountsUpdate(UserHandle userHandle) {
if (!accountExists()) {
// The account was deleted. Pop back.
goBack();
}
}
/** Returns whether the account being shown by this fragment still exists. */
@VisibleForTesting
boolean accountExists() {
if (mAccount == null) {
return false;
}
Account[] accounts = AccountManager.get(getContext()).getAccountsByTypeAsUser(mAccount.type,
mUserInfo.getUserHandle());
return Arrays.asList(accounts).contains(mAccount);
}
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS;
import static com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment.DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG;
import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByDpm;
import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.CallSuper;
import androidx.annotation.VisibleForTesting;
import com.android.car.settings.R;
import com.android.car.settings.common.ActivityResultCallback;
import com.android.car.settings.common.ConfirmationDialogFragment;
import com.android.car.settings.common.ErrorDialog;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.enterprise.EnterpriseUtils;
import com.android.car.settings.profiles.ProfileHelper;
import java.io.IOException;
/**
* Controller for the preference that shows the details of an account. It also handles a secondary
* button for account removal.
*/
public class AccountDetailsPreferenceController extends AccountDetailsBasePreferenceController
implements ActivityResultCallback {
private static final Logger LOG = new Logger(AccountDetailsPreferenceController.class);
private static final int REMOVE_ACCOUNT_REQUEST = 101;
private AccountManagerCallback<Bundle> mCallback =
future -> {
// If already out of this screen, don't proceed.
if (!isStarted()) {
return;
}
boolean done = true;
boolean success = false;
try {
Bundle result = future.getResult();
Intent removeIntent = result.getParcelable(AccountManager.KEY_INTENT);
if (removeIntent != null) {
done = false;
getFragmentController().startActivityForResult(new Intent(removeIntent),
REMOVE_ACCOUNT_REQUEST, this);
} else {
success = future.getResult().getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
}
} catch (OperationCanceledException | IOException | AuthenticatorException e) {
LOG.v("removeAccount error: " + e);
}
if (done) {
if (!success) {
showErrorDialog();
} else {
getFragmentController().goBack();
}
}
};
private ConfirmationDialogFragment.ConfirmListener mConfirmListener = arguments -> {
AccountManager.get(getContext()).removeAccountAsUser(getAccount(), /* activity= */ null,
mCallback, null, getUserHandle());
ConfirmationDialogFragment dialog =
(ConfirmationDialogFragment) getFragmentController().findDialogByTag(
ConfirmationDialogFragment.TAG);
if (dialog != null) {
dialog.dismiss();
}
};
public AccountDetailsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
@CallSuper
protected void onCreateInternal() {
super.onCreateInternal();
ConfirmationDialogFragment dialog =
(ConfirmationDialogFragment) getFragmentController().findDialogByTag(
ConfirmationDialogFragment.TAG);
ConfirmationDialogFragment.resetListeners(
dialog,
mConfirmListener,
/* rejectListener= */ null,
/* neutralListener= */ null);
getPreference().setSecondaryActionVisible(
getSecondaryActionAvailabilityStatus() == AVAILABLE
|| getSecondaryActionAvailabilityStatus() == AVAILABLE_FOR_VIEWING);
getPreference().setSecondaryActionEnabled(
getSecondaryActionAvailabilityStatus() != DISABLED_FOR_PROFILE);
getPreference().setOnSecondaryActionClickListener(this::onRemoveAccountClicked);
}
@Override
public void processActivityResult(int requestCode, int resultCode, Intent data) {
if (!isStarted()) {
return;
}
if (requestCode == REMOVE_ACCOUNT_REQUEST) {
// Activity result code may not adequately reflect the account removal status, so
// KEY_BOOLEAN_RESULT is used here instead. If the intent does not have this value
// included, a no-op will be performed and the account update listener in
// {@link AccountDetailsFragment} will still handle the back navigation upon removal.
if (data != null && data.hasExtra(AccountManager.KEY_BOOLEAN_RESULT)) {
boolean success = data.getBooleanExtra(AccountManager.KEY_BOOLEAN_RESULT, false);
if (success) {
getFragmentController().goBack();
} else {
showErrorDialog();
}
}
}
}
private int getSecondaryActionAvailabilityStatus() {
ProfileHelper profileHelper = getProfileHelper();
if (profileHelper.canCurrentProcessModifyAccounts()) {
return AVAILABLE;
}
if (profileHelper.isDemoOrGuest()
|| hasUserRestrictionByUm(getContext(), DISALLOW_MODIFY_ACCOUNTS)) {
return DISABLED_FOR_PROFILE;
}
return AVAILABLE_FOR_VIEWING;
}
private void onRemoveAccountClicked() {
if (hasUserRestrictionByDpm(getContext(), DISALLOW_MODIFY_ACCOUNTS)) {
showActionDisabledByAdminDialog();
}
ConfirmationDialogFragment dialog =
new ConfirmationDialogFragment.Builder(getContext())
.setTitle(R.string.really_remove_account_title)
.setMessage(R.string.really_remove_account_message)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.remove_account_title, mConfirmListener)
.build();
getFragmentController().showDialog(dialog, ConfirmationDialogFragment.TAG);
}
private void showErrorDialog() {
getFragmentController().showDialog(
ErrorDialog.newInstance(R.string.remove_account_error_title), /* tag= */ null);
}
private void showActionDisabledByAdminDialog() {
getFragmentController().showDialog(
EnterpriseUtils.getActionDisabledByAdminDialog(getContext(),
DISALLOW_MODIFY_ACCOUNTS),
DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG);
}
@VisibleForTesting
ProfileHelper getProfileHelper() {
return ProfileHelper.getInstance(getContext());
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import android.accounts.Account;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.CallSuper;
import androidx.preference.Preference;
import com.android.car.settings.common.ExtraSettingsPreferenceController;
import com.android.car.settings.common.FragmentController;
import java.util.Map;
/**
* Injects preferences from other system applications at a placeholder location into the account
* details fragment. This class is and extension of {@link ExtraSettingsPreferenceController} which
* is needed to check what all preferences to show in the account details page.
*/
public class AccountDetailsSettingController extends ExtraSettingsPreferenceController {
private static final String METADATA_IA_ACCOUNT = "com.android.settings.ia.account";
private Account mAccount;
public AccountDetailsSettingController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions restrictionInfo) {
super(context, preferenceKey, fragmentController, restrictionInfo);
}
/** Sets the account that the preferences are being shown for. */
public void setAccount(Account account) {
mAccount = account;
}
@Override
@CallSuper
protected void checkInitialized() {
if (mAccount == null) {
throw new IllegalStateException(
"AccountDetailsSettingController must be initialized by calling "
+ "setAccount(Account)");
}
}
@Override
@CallSuper
protected void addExtraSettings(Map<Preference, Bundle> preferenceBundleMap) {
for (Preference setting : preferenceBundleMap.keySet()) {
if (mAccount != null && !mAccount.type.equals(
preferenceBundleMap.get(setting).getString(METADATA_IA_ACCOUNT))) {
continue;
}
getPreference().addPreference(setting);
}
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncAdapterType;
import android.content.SyncInfo;
import android.content.SyncStatusInfo;
import android.content.SyncStatusObserver;
import androidx.annotation.VisibleForTesting;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.ui.preference.CarUiTwoActionIconPreference;
import com.android.settingslib.utils.ThreadUtils;
import java.util.List;
import java.util.Set;
/**
* Controller for the preference that shows information about an account, including info about
* failures. It also handles a secondary button for account syncing.
*/
public class AccountDetailsWithSyncStatusPreferenceController extends
AccountDetailsBasePreferenceController {
private Object mStatusChangeListenerHandle;
private SyncStatusObserver mSyncStatusObserver =
which -> ThreadUtils.postOnMainThread(() -> {
// The observer call may occur even if the fragment hasn't been started, so
// only force an update if the fragment hasn't been stopped.
if (isStarted()) {
refreshUi();
}
});
public AccountDetailsWithSyncStatusPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
/**
* Registers the account update and sync status change callbacks.
*/
@Override
protected void onStartInternal() {
mStatusChangeListenerHandle = ContentResolver.addStatusChangeListener(
ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
| ContentResolver.SYNC_OBSERVER_TYPE_STATUS
| ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, mSyncStatusObserver);
}
/**
* Unregisters the account update and sync status change callbacks.
*/
@Override
protected void onStopInternal() {
if (mStatusChangeListenerHandle != null) {
ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle);
}
}
@Override
protected void updateState(CarUiTwoActionIconPreference preference) {
super.updateState(preference);
if (isSyncFailing()) {
preference.setSummary(R.string.sync_is_failing);
} else {
preference.setSummary("");
}
updateSyncButton();
}
private boolean isSyncFailing() {
int userId = getUserHandle().getIdentifier();
List<SyncInfo> currentSyncs = getCurrentSyncs(userId);
boolean syncIsFailing = false;
Set<SyncAdapterType> syncAdapters = AccountSyncHelper.getVisibleSyncAdaptersForAccount(
getContext(), getAccount(), getUserHandle());
for (SyncAdapterType syncAdapter : syncAdapters) {
String authority = syncAdapter.authority;
SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(getAccount(), authority,
userId);
boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(getAccount(),
authority, userId);
boolean activelySyncing = AccountSyncHelper.isSyncing(getAccount(), currentSyncs,
authority);
AccountSyncHelper.SyncState syncState = AccountSyncHelper.getSyncState(status,
syncEnabled, activelySyncing);
boolean syncIsPending = status != null && status.pending;
if (syncState == AccountSyncHelper.SyncState.FAILED && !activelySyncing
&& !syncIsPending) {
syncIsFailing = true;
}
}
return syncIsFailing;
}
private void updateSyncButton() {
// Set the button to either request or cancel sync, depending on the current state
boolean hasActiveSyncs = !getCurrentSyncs(
getUserHandle().getIdentifier()).isEmpty();
// If there are active syncs, clicking the button with cancel them. Otherwise, clicking the
// button will start them.
getPreference().setSecondaryActionIcon(
hasActiveSyncs ? R.drawable.ic_sync_cancel : R.drawable.ic_sync);
getPreference().setOnSecondaryActionClickListener(hasActiveSyncs
? this::cancelSyncForEnabledProviders
: this::requestSyncForEnabledProviders);
}
private void requestSyncForEnabledProviders() {
int userId = getUserHandle().getIdentifier();
Set<SyncAdapterType> adapters = AccountSyncHelper.getSyncableSyncAdaptersForAccount(
getAccount(), getUserHandle());
for (SyncAdapterType adapter : adapters) {
requestSync(adapter.authority, userId);
}
}
private void cancelSyncForEnabledProviders() {
int userId = getUserHandle().getIdentifier();
Set<SyncAdapterType> adapters = AccountSyncHelper.getSyncableSyncAdaptersForAccount(
getAccount(), getUserHandle());
for (SyncAdapterType adapter : adapters) {
cancelSync(adapter.authority, userId);
}
}
@VisibleForTesting
List<SyncInfo> getCurrentSyncs(int userId) {
return ContentResolver.getCurrentSyncsAsUser(userId);
}
@VisibleForTesting
void requestSync(String authority, int userId) {
AccountSyncHelper.requestSyncIfAllowed(getAccount(), authority, userId);
}
@VisibleForTesting
void cancelSync(String authority, int userId) {
ContentResolver.cancelSyncAsUser(getAccount(), authority, userId);
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS;
import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceGroup;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.profiles.ProfileDetailsBasePreferenceController;
import com.android.car.settings.profiles.ProfileHelper;
/**
* Controller for displaying accounts and associated actions.
* Ensures changes can only be made by the appropriate users.
*/
public class AccountGroupPreferenceController extends
ProfileDetailsBasePreferenceController<PreferenceGroup> {
public AccountGroupPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<PreferenceGroup> getPreferenceType() {
return PreferenceGroup.class;
}
@Override
protected void onCreateInternal() {
super.onCreateInternal();
setClickableWhileDisabled(getPreference(), /* clickable= */ true, p -> getProfileHelper()
.runClickableWhileDisabled(getContext(), getFragmentController()));
}
@Override
protected int getDefaultAvailabilityStatus() {
ProfileHelper profileHelper = getProfileHelper();
boolean isCurrentUser = profileHelper.isCurrentProcessUser(getUserInfo());
boolean canModifyAccounts = getProfileHelper().canCurrentProcessModifyAccounts();
if (isCurrentUser && canModifyAccounts) {
return AVAILABLE;
}
if (!isCurrentUser || profileHelper.isDemoOrGuest()
|| hasUserRestrictionByUm(getContext(), DISALLOW_MODIFY_ACCOUNTS)) {
return DISABLED_FOR_PROFILE;
}
return AVAILABLE_FOR_VIEWING;
}
@VisibleForTesting
ProfileHelper getProfileHelper() {
return ProfileHelper.getInstance(getContext());
}
}

View File

@@ -0,0 +1,345 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS;
import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.car.drivingstate.CarUxRestrictions;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.UserInfo;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import androidx.collection.ArrayMap;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.car.settings.profiles.ProfileHelper;
import com.android.car.settings.profiles.ProfileUtils;
import com.android.car.ui.preference.CarUiPreference;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.accounts.AuthenticatorHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Controller for listing accounts.
*
* <p>Largely derived from {@link com.android.settings.accounts.AccountPreferenceController}
*/
public class AccountListPreferenceController extends
PreferenceController<PreferenceCategory> implements
AuthenticatorHelper.OnAccountsUpdateListener {
private static final String NO_ACCOUNT_PREF_KEY = "no_accounts_added";
private final ArrayMap<String, Preference> mPreferences = new ArrayMap<>();
private UserInfo mUserInfo;
private AuthenticatorHelper mAuthenticatorHelper;
private String[] mAuthorities;
private boolean mListenerRegistered = false;
private boolean mNeedToRefreshUserInfo = false;
private final BroadcastReceiver mUserUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onUsersUpdate();
}
};
public AccountListPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mUserInfo = ProfileHelper.getInstance(context).getCurrentProcessUserInfo();
mAuthenticatorHelper = createAuthenticatorHelper();
}
/** Sets the account authorities that are available. */
public void setAuthorities(String[] authorities) {
mAuthorities = authorities;
}
@Override
protected Class<PreferenceCategory> getPreferenceType() {
return PreferenceCategory.class;
}
@Override
protected void updateState(PreferenceCategory preference) {
forceUpdateAccountsCategory();
}
@Override
protected void onCreateInternal() {
super.onCreateInternal();
setClickableWhileDisabled(getPreference(), /* clickable= */ true, p -> getProfileHelper()
.runClickableWhileDisabled(getContext(), getFragmentController()));
}
@Override
protected int getDefaultAvailabilityStatus() {
ProfileHelper profileHelper = getProfileHelper();
boolean canModifyAccounts = profileHelper.canCurrentProcessModifyAccounts();
if (canModifyAccounts) {
return AVAILABLE;
}
if (profileHelper.isDemoOrGuest()
|| hasUserRestrictionByUm(getContext(), DISALLOW_MODIFY_ACCOUNTS)) {
return DISABLED_FOR_PROFILE;
}
return AVAILABLE_FOR_VIEWING;
}
/**
* Registers the account update and user update callbacks.
*/
@Override
protected void onStartInternal() {
mAuthenticatorHelper.listenToAccountUpdates();
registerForUserEvents();
mListenerRegistered = true;
/* refresh UserInfo only when restarting */
if (mNeedToRefreshUserInfo) {
onUsersUpdate();
}
}
/**
* Unregisters the account update and user update callbacks.
*/
@Override
protected void onStopInternal() {
mAuthenticatorHelper.stopListeningToAccountUpdates();
unregisterForUserEvents();
mListenerRegistered = false;
mNeedToRefreshUserInfo = true;
}
@Override
public void onAccountsUpdate(UserHandle userHandle) {
if (userHandle.equals(mUserInfo.getUserHandle())) {
forceUpdateAccountsCategory();
}
}
@VisibleForTesting
void onUsersUpdate() {
mUserInfo = ProfileUtils.getUserInfo(getContext(), mUserInfo.id);
forceUpdateAccountsCategory();
}
@VisibleForTesting
AuthenticatorHelper createAuthenticatorHelper() {
return new AuthenticatorHelper(getContext(), mUserInfo.getUserHandle(), this);
}
private boolean onAccountPreferenceClicked(AccountPreference preference) {
// Show the account's details when an account is clicked on.
getFragmentController().launchFragment(AccountDetailsFragment.newInstance(
preference.getAccount(), preference.getLabel(), mUserInfo));
return true;
}
/** Forces a refresh of the account preferences. */
private void forceUpdateAccountsCategory() {
// Set the category title and include the user's name
getPreference().setTitle(
getContext().getString(R.string.account_list_title, mUserInfo.name));
// Recreate the authentication helper to refresh the list of enabled accounts
mAuthenticatorHelper.stopListeningToAccountUpdates();
mAuthenticatorHelper = createAuthenticatorHelper();
if (mListenerRegistered) {
mAuthenticatorHelper.listenToAccountUpdates();
}
Set<String> preferencesToRemove = new HashSet<>(mPreferences.keySet());
List<? extends Preference> preferences = getAccountPreferences(preferencesToRemove);
// Add all preferences that aren't already shown. Manually set the order so that existing
// preferences are reordered correctly.
for (int i = 0; i < preferences.size(); i++) {
Preference pref = preferences.get(i);
pref.setOrder(i);
mPreferences.put(pref.getKey(), pref);
getPreference().addPreference(pref);
}
for (String key : preferencesToRemove) {
getPreference().removePreference(mPreferences.get(key));
mPreferences.remove(key);
}
}
/**
* Returns a list of preferences corresponding to the accounts for the current user.
*
* <p> Derived from
* {@link com.android.settings.accounts.AccountPreferenceController#getAccountTypePreferences}
*
* @param preferencesToRemove the current preferences shown; only preferences to be removed will
* remain after method execution
*/
private List<? extends Preference> getAccountPreferences(
Set<String> preferencesToRemove) {
String[] accountTypes = mAuthenticatorHelper.getEnabledAccountTypes();
ArrayList<AccountPreference> accountPreferences =
new ArrayList<>(accountTypes.length);
for (int i = 0; i < accountTypes.length; i++) {
String accountType = accountTypes[i];
// Skip showing any account that does not have any of the requested authorities
if (!accountTypeHasAnyRequestedAuthorities(accountType)) {
continue;
}
CharSequence label = mAuthenticatorHelper.getLabelForType(getContext(), accountType);
if (label == null) {
continue;
}
Account[] accounts = AccountManager.get(getContext())
.getAccountsByTypeAsUser(accountType, mUserInfo.getUserHandle());
Drawable icon = mAuthenticatorHelper.getDrawableForType(getContext(), accountType);
// Add a preference row for each individual account
for (Account account : accounts) {
String key = AccountPreference.buildKey(account);
AccountPreference preference = (AccountPreference) mPreferences.getOrDefault(key,
new AccountPreference(getContext(), account, label, icon));
preference.setOnPreferenceClickListener(
(Preference pref) -> onAccountPreferenceClicked((AccountPreference) pref));
accountPreferences.add(preference);
preferencesToRemove.remove(key);
}
mAuthenticatorHelper.preloadDrawableForType(getContext(), accountType);
}
// If there are no accounts, return the "no account added" preference.
if (accountPreferences.isEmpty()) {
preferencesToRemove.remove(NO_ACCOUNT_PREF_KEY);
return Arrays.asList(mPreferences.getOrDefault(NO_ACCOUNT_PREF_KEY,
createNoAccountsAddedPreference()));
}
Collections.sort(accountPreferences, Comparator.comparing(
(AccountPreference a) -> a.getSummary().toString())
.thenComparing((AccountPreference a) -> a.getTitle().toString()));
return accountPreferences;
}
private Preference createNoAccountsAddedPreference() {
CarUiPreference emptyPreference = new CarUiPreference(getContext());
emptyPreference.setTitle(R.string.no_accounts_added);
emptyPreference.setKey(NO_ACCOUNT_PREF_KEY);
emptyPreference.setSelectable(false);
return emptyPreference;
}
private void registerForUserEvents() {
IntentFilter filter = new IntentFilter(Intent.ACTION_USER_INFO_CHANGED);
getContext().registerReceiver(mUserUpdateReceiver, filter);
}
private void unregisterForUserEvents() {
getContext().unregisterReceiver(mUserUpdateReceiver);
}
/**
* Returns whether the account type has any of the authorities requested by the caller.
*
* <p> Derived from {@link AccountPreferenceController#accountTypeHasAnyRequestedAuthorities}
*/
private boolean accountTypeHasAnyRequestedAuthorities(String accountType) {
if (mAuthorities == null || mAuthorities.length == 0) {
// No authorities required
return true;
}
ArrayList<String> authoritiesForType =
mAuthenticatorHelper.getAuthoritiesForAccountType(accountType);
if (authoritiesForType == null) {
return false;
}
for (int j = 0; j < mAuthorities.length; j++) {
if (authoritiesForType.contains(mAuthorities[j])) {
return true;
}
}
return false;
}
@VisibleForTesting
ProfileHelper getProfileHelper() {
return ProfileHelper.getInstance(getContext());
}
private static class AccountPreference extends CarUiPreference {
/** Account that this Preference represents. */
private final Account mAccount;
private final CharSequence mLabel;
private AccountPreference(Context context, Account account, CharSequence label,
Drawable icon) {
super(context);
mAccount = account;
mLabel = label;
setKey(buildKey(account));
setTitle(account.name);
setSummary(label);
setIcon(icon);
setShowChevron(false);
}
/**
* Build a unique preference key based on the account.
*/
public static String buildKey(Account account) {
return String.valueOf(account.hashCode());
}
public Account getAccount() {
return mAccount;
}
public CharSequence getLabel() {
return mLabel;
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import android.accounts.Account;
import android.content.Context;
import android.os.Bundle;
import android.os.UserHandle;
import androidx.annotation.XmlRes;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
/**
* Shows details for syncing an account.
*/
public class AccountSyncDetailsFragment extends SettingsFragment {
private static final String EXTRA_ACCOUNT = "extra_account";
private static final String EXTRA_USER_HANDLE = "extra_user_handle";
/**
* Creates a new AccountSyncDetailsFragment.
*
* <p>Passes the provided account and user handle to the fragment via fragment arguments.
*/
public static AccountSyncDetailsFragment newInstance(Account account, UserHandle userHandle) {
AccountSyncDetailsFragment accountSyncDetailsFragment = new AccountSyncDetailsFragment();
Bundle bundle = new Bundle();
bundle.putParcelable(EXTRA_ACCOUNT, account);
bundle.putParcelable(EXTRA_USER_HANDLE, userHandle);
accountSyncDetailsFragment.setArguments(bundle);
return accountSyncDetailsFragment;
}
@Override
@XmlRes
protected int getPreferenceScreenResId() {
return R.xml.account_sync_details_fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
Account account = getArguments().getParcelable(EXTRA_ACCOUNT);
UserHandle userHandle = getArguments().getParcelable(EXTRA_USER_HANDLE);
use(AccountDetailsWithSyncStatusPreferenceController.class,
R.string.pk_account_details_with_sync)
.setAccount(account);
use(AccountDetailsWithSyncStatusPreferenceController.class,
R.string.pk_account_details_with_sync)
.setUserHandle(userHandle);
use(AccountSyncDetailsPreferenceController.class, R.string.pk_account_sync_details)
.setAccount(account);
use(AccountSyncDetailsPreferenceController.class, R.string.pk_account_sync_details)
.setUserHandle(userHandle);
}
}

View File

@@ -0,0 +1,385 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.SyncAdapterType;
import android.content.SyncInfo;
import android.content.SyncStatusInfo;
import android.content.SyncStatusObserver;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.text.format.DateFormat;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.collection.ArrayMap;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.PreferenceController;
import com.android.settingslib.accounts.AuthenticatorHelper;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Controller that presents all visible sync adapters for an account.
*
* <p>Largely derived from {@link com.android.settings.accounts.AccountSyncSettings}.
*/
public class AccountSyncDetailsPreferenceController extends
PreferenceController<PreferenceGroup> implements
AuthenticatorHelper.OnAccountsUpdateListener {
private static final Logger LOG = new Logger(AccountSyncDetailsPreferenceController.class);
/**
* Preferences are keyed by authority so that existing SyncPreferences can be reused on account
* sync.
*/
private final Map<String, SyncPreference> mSyncPreferences = new ArrayMap<>();
private Account mAccount;
private UserHandle mUserHandle;
private AuthenticatorHelper mAuthenticatorHelper;
private Object mStatusChangeListenerHandle;
private SyncStatusObserver mSyncStatusObserver =
which -> ThreadUtils.postOnMainThread(() -> {
// The observer call may occur even if the fragment hasn't been started, so
// only force an update if the fragment hasn't been stopped.
if (isStarted()) {
forceUpdateSyncCategory();
}
});
public AccountSyncDetailsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
/** Sets the account that the sync preferences are being shown for. */
public void setAccount(Account account) {
mAccount = account;
}
/** Sets the user handle used by the controller. */
public void setUserHandle(UserHandle userHandle) {
mUserHandle = userHandle;
}
@Override
protected Class<PreferenceGroup> getPreferenceType() {
return PreferenceGroup.class;
}
/**
* Verifies that the controller was properly initialized with {@link #setAccount(Account)} and
* {@link #setUserHandle(UserHandle)}.
*
* @throws IllegalStateException if the account or user handle is {@code null}
*/
@Override
protected void checkInitialized() {
LOG.v("checkInitialized");
if (mAccount == null) {
throw new IllegalStateException(
"AccountSyncDetailsPreferenceController must be initialized by calling "
+ "setAccount(Account)");
}
if (mUserHandle == null) {
throw new IllegalStateException(
"AccountSyncDetailsPreferenceController must be initialized by calling "
+ "setUserHandle(UserHandle)");
}
}
/**
* Initializes the authenticator helper.
*/
@Override
protected void onCreateInternal() {
mAuthenticatorHelper = new AuthenticatorHelper(getContext(), mUserHandle, /* listener= */
this);
}
/**
* Registers the account update and sync status change callbacks.
*/
@Override
protected void onStartInternal() {
mAuthenticatorHelper.listenToAccountUpdates();
mStatusChangeListenerHandle = ContentResolver.addStatusChangeListener(
ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
| ContentResolver.SYNC_OBSERVER_TYPE_STATUS
| ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, mSyncStatusObserver);
}
/**
* Unregisters the account update and sync status change callbacks.
*/
@Override
protected void onStopInternal() {
mAuthenticatorHelper.stopListeningToAccountUpdates();
if (mStatusChangeListenerHandle != null) {
ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle);
}
}
@Override
public void onAccountsUpdate(UserHandle userHandle) {
// Only force a refresh if accounts have changed for the current user.
if (userHandle.equals(mUserHandle)) {
forceUpdateSyncCategory();
}
}
@Override
public void updateState(PreferenceGroup preferenceGroup) {
// Add preferences for each account if the controller should be available
forceUpdateSyncCategory();
}
/**
* Handles toggling/syncing when a sync preference is clicked on.
*
* <p>Largely derived from
* {@link com.android.settings.accounts.AccountSyncSettings#onPreferenceTreeClick}.
*/
private boolean onSyncPreferenceClicked(SyncPreference preference) {
String authority = preference.getKey();
String packageName = preference.getPackageName();
int uid = preference.getUid();
if (preference.isOneTimeSyncMode()) {
// If the sync adapter doesn't have access to the account we either
// request access by starting an activity if possible or kick off the
// sync which will end up posting an access request notification.
if (requestAccountAccessIfNeeded(packageName, uid)) {
return true;
}
requestSync(authority);
} else {
boolean syncOn = preference.isChecked();
int userId = mUserHandle.getIdentifier();
boolean oldSyncState = ContentResolver.getSyncAutomaticallyAsUser(mAccount,
authority, userId);
if (syncOn != oldSyncState) {
// Toggling this switch triggers sync but we may need a user approval. If the
// sync adapter doesn't have access to the account we either request access by
// starting an activity if possible or kick off the sync which will end up
// posting an access request notification.
if (syncOn && requestAccountAccessIfNeeded(packageName, uid)) {
return true;
}
// If we're enabling sync, this will request a sync as well.
ContentResolver.setSyncAutomaticallyAsUser(mAccount, authority, syncOn, userId);
if (syncOn) {
requestSync(authority);
} else {
cancelSync(authority);
}
}
}
return true;
}
private void requestSync(String authority) {
AccountSyncHelper.requestSyncIfAllowed(mAccount, authority, mUserHandle.getIdentifier());
}
private void cancelSync(String authority) {
ContentResolver.cancelSyncAsUser(mAccount, authority, mUserHandle.getIdentifier());
}
/**
* Requests account access if needed.
*
* <p>Copied from
* {@link com.android.settings.accounts.AccountSyncSettings#requestAccountAccessIfNeeded}.
*/
private boolean requestAccountAccessIfNeeded(String packageName, int uid) {
if (packageName == null) {
return false;
}
AccountManager accountManager = getContext().getSystemService(AccountManager.class);
if (!accountManager.hasAccountAccess(mAccount, packageName, mUserHandle)) {
IntentSender intent = accountManager.createRequestAccountAccessIntentSenderAsUser(
mAccount, packageName, mUserHandle);
if (intent != null) {
try {
getFragmentController().startIntentSenderForResult(intent,
uid, /* fillInIntent= */ null, /* flagsMask= */ 0,
/* flagsValues= */ 0, /* options= */ null,
this::onAccountRequestApproved);
return true;
} catch (IntentSender.SendIntentException e) {
LOG.e("Error requesting account access", e);
}
}
}
return false;
}
/** Handles a sync adapter refresh when an account request was approved. */
public void onAccountRequestApproved(int uid, int resultCode, @Nullable Intent data) {
if (resultCode == Activity.RESULT_OK) {
for (SyncPreference pref : mSyncPreferences.values()) {
if (pref.getUid() == uid) {
onSyncPreferenceClicked(pref);
return;
}
}
}
}
/** Forces a refresh of the sync adapter preferences. */
private void forceUpdateSyncCategory() {
Set<String> preferencesToRemove = new HashSet<>(mSyncPreferences.keySet());
List<SyncPreference> preferences = getSyncPreferences(preferencesToRemove);
// Sort the preferences, add the ones that need to be added, and remove the ones that need
// to be removed. Manually set the order so that existing preferences are reordered
// correctly.
Collections.sort(preferences, Comparator.comparing(
(SyncPreference a) -> a.getTitle().toString())
.thenComparing((SyncPreference a) -> a.getSummary().toString()));
for (int i = 0; i < preferences.size(); i++) {
SyncPreference pref = preferences.get(i);
pref.setOrder(i);
mSyncPreferences.put(pref.getKey(), pref);
getPreference().addPreference(pref);
}
for (String key : preferencesToRemove) {
getPreference().removePreference(mSyncPreferences.get(key));
mSyncPreferences.remove(key);
}
}
/**
* Returns a list of preferences corresponding to the visible sync adapters for the current
* user.
*
* <p> Derived from {@link com.android.settings.accounts.AccountSyncSettings#setFeedsState}
* and {@link com.android.settings.accounts.AccountSyncSettings#updateAccountSwitches}.
*
* @param preferencesToRemove the keys for the preferences currently being shown; only the keys
* for preferences to be removed will remain after method execution
*/
private List<SyncPreference> getSyncPreferences(Set<String> preferencesToRemove) {
int userId = mUserHandle.getIdentifier();
PackageManager packageManager = getContext().getPackageManager();
List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncsAsUser(userId);
// Whether one time sync is enabled rather than automtic sync
boolean oneTimeSyncMode = !ContentResolver.getMasterSyncAutomaticallyAsUser(userId);
List<SyncPreference> syncPreferences = new ArrayList<>();
Set<SyncAdapterType> syncAdapters = AccountSyncHelper.getVisibleSyncAdaptersForAccount(
getContext(), mAccount, mUserHandle);
for (SyncAdapterType syncAdapter : syncAdapters) {
String authority = syncAdapter.authority;
int uid;
try {
uid = packageManager.getPackageUidAsUser(syncAdapter.getPackageName(), userId);
} catch (PackageManager.NameNotFoundException e) {
LOG.e("No uid for package" + syncAdapter.getPackageName(), e);
// If we can't get the Uid for the package hosting the sync adapter, don't show it
continue;
}
// If we've reached this point, the sync adapter should be shown. If a preference for
// the sync adapter already exists, update its state. Otherwise, create a new
// preference.
SyncPreference pref = mSyncPreferences.getOrDefault(authority,
new SyncPreference(getContext(), authority));
pref.setUid(uid);
pref.setPackageName(syncAdapter.getPackageName());
pref.setOnPreferenceClickListener(
(Preference p) -> onSyncPreferenceClicked((SyncPreference) p));
CharSequence title = AccountSyncHelper.getTitle(getContext(), authority, mUserHandle);
pref.setTitle(title);
// Keep track of preferences that need to be added and removed
syncPreferences.add(pref);
preferencesToRemove.remove(authority);
SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(mAccount, authority,
userId);
boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(mAccount, authority,
userId);
boolean activelySyncing = AccountSyncHelper.isSyncing(mAccount, currentSyncs,
authority);
// The preference should be checked if one one-time sync or regular sync is enabled
boolean checked = oneTimeSyncMode || syncEnabled;
pref.setChecked(checked);
String summary = getSummary(status, syncEnabled, activelySyncing);
pref.setSummary(summary);
// Update the sync state so the icon is updated
AccountSyncHelper.SyncState syncState = AccountSyncHelper.getSyncState(status,
syncEnabled, activelySyncing);
pref.setSyncState(syncState);
pref.setOneTimeSyncMode(oneTimeSyncMode);
}
return syncPreferences;
}
private String getSummary(SyncStatusInfo status, boolean syncEnabled, boolean activelySyncing) {
long successEndTime = (status == null) ? 0 : status.lastSuccessTime;
// Set the summary based on the current syncing state
if (!syncEnabled) {
return getContext().getString(R.string.sync_disabled);
} else if (activelySyncing) {
return getContext().getString(R.string.sync_in_progress);
} else if (successEndTime != 0) {
Date date = new Date();
date.setTime(successEndTime);
String timeString = formatSyncDate(date);
return getContext().getString(R.string.last_synced, timeString);
}
return "";
}
@VisibleForTesting
String formatSyncDate(Date date) {
return DateFormat.getDateFormat(getContext()).format(date) + " " + DateFormat.getTimeFormat(
getContext()).format(date);
}
}

View File

@@ -0,0 +1,195 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import android.accounts.Account;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncAdapterType;
import android.content.SyncInfo;
import android.content.SyncStatusInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.os.Bundle;
import android.os.UserHandle;
import android.text.TextUtils;
import androidx.annotation.VisibleForTesting;
import com.android.car.settings.common.Logger;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Helper that provides utility methods for account syncing. */
class AccountSyncHelper {
private static final Logger LOG = new Logger(AccountSyncHelper.class);
private AccountSyncHelper() {
}
/** Returns the visible sync adapters available for an account. */
static Set<SyncAdapterType> getVisibleSyncAdaptersForAccount(Context context, Account account,
UserHandle userHandle) {
Set<SyncAdapterType> syncableAdapters = getSyncableSyncAdaptersForAccount(account,
userHandle);
syncableAdapters.removeIf(
(SyncAdapterType syncAdapter) -> !isVisible(context, syncAdapter, userHandle));
return syncableAdapters;
}
/** Returns the syncable sync adapters available for an account. */
static Set<SyncAdapterType> getSyncableSyncAdaptersForAccount(Account account,
UserHandle userHandle) {
Set<SyncAdapterType> adapters = new HashSet<>();
SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(
userHandle.getIdentifier());
for (int i = 0; i < syncAdapters.length; i++) {
SyncAdapterType syncAdapter = syncAdapters[i];
String authority = syncAdapter.authority;
// If the sync adapter is not for this account type, don't include it
if (!syncAdapter.accountType.equals(account.type)) {
continue;
}
boolean isSyncable = ContentResolver.getIsSyncableAsUser(account, authority,
userHandle.getIdentifier()) > 0;
// If the adapter is not syncable, don't include it
if (!isSyncable) {
continue;
}
adapters.add(syncAdapter);
}
return adapters;
}
/**
* Requests a sync if it is allowed.
*
* <p>Derived from
* {@link com.android.settings.accounts.AccountSyncSettings#requestOrCancelSync}.
*
* @return {@code true} if sync was requested, {@code false} otherwise.
*/
static boolean requestSyncIfAllowed(Account account, String authority, int userId) {
if (!syncIsAllowed(account, authority, userId)) {
return false;
}
Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
ContentResolver.requestSyncAsUser(account, authority, userId, extras);
return true;
}
/**
* Returns the label for a given sync authority.
*
* @return the title if available, and an empty CharSequence otherwise
*/
static CharSequence getTitle(Context context, String authority, UserHandle userHandle) {
PackageManager packageManager = context.getPackageManager();
ProviderInfo providerInfo = packageManager.resolveContentProviderAsUser(
authority, /* flags= */ 0, userHandle.getIdentifier());
if (providerInfo == null) {
return "";
}
return providerInfo.loadLabel(packageManager);
}
/** Returns whether a sync adapter is currently syncing for the account being shown. */
static boolean isSyncing(Account account, List<SyncInfo> currentSyncs, String authority) {
for (SyncInfo syncInfo : currentSyncs) {
if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) {
return true;
}
}
return false;
}
/** Returns the current sync state based on sync status information. */
static SyncState getSyncState(SyncStatusInfo status, boolean syncEnabled,
boolean activelySyncing) {
boolean initialSync = status != null && status.initialize;
boolean syncIsPending = status != null && status.pending;
boolean lastSyncFailed = syncEnabled && status != null && status.lastFailureTime != 0
&& status.getLastFailureMesgAsInt(0)
!= ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS;
if (activelySyncing && !initialSync) {
return SyncState.ACTIVE;
} else if (syncIsPending && !initialSync) {
return SyncState.PENDING;
} else if (lastSyncFailed) {
return SyncState.FAILED;
}
return SyncState.NONE;
}
@VisibleForTesting
static boolean syncIsAllowed(Account account, String authority, int userId) {
boolean oneTimeSyncMode = !ContentResolver.getMasterSyncAutomaticallyAsUser(userId);
boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(account, authority,
userId);
return oneTimeSyncMode || syncEnabled;
}
private static boolean isVisible(Context context, SyncAdapterType syncAdapter,
UserHandle userHandle) {
String authority = syncAdapter.authority;
if (!syncAdapter.isUserVisible()) {
// If the sync adapter is not visible, don't show it
return false;
}
try {
context.getPackageManager().getPackageUidAsUser(syncAdapter.getPackageName(),
userHandle.getIdentifier());
} catch (PackageManager.NameNotFoundException e) {
LOG.e("No uid for package" + syncAdapter.getPackageName(), e);
// If we can't get the Uid for the package hosting the sync adapter, don't show it
return false;
}
CharSequence title = getTitle(context, authority, userHandle);
if (TextUtils.isEmpty(title)) {
return false;
}
return true;
}
/** Denotes a sync adapter state. */
public enum SyncState {
/** The sync adapter is actively syncing. */
ACTIVE,
/** The sync adapter is waiting to start syncing. */
PENDING,
/** The sync adapter's last attempt to sync failed. */
FAILED,
/** Nothing to note about the sync adapter's sync state. */
NONE;
}
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import android.accounts.Account;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncAdapterType;
import android.os.UserHandle;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.PreferenceController;
/**
* Controller for the account syncing preference.
*
* <p>Largely derived from {@link com.android.settings.accounts.AccountSyncPreferenceController}.
*/
public class AccountSyncPreferenceController extends PreferenceController<Preference> {
private static final Logger LOG = new Logger(AccountSyncPreferenceController.class);
private Account mAccount;
private UserHandle mUserHandle;
public AccountSyncPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
/** Sets the account that the sync preferences are being shown for. */
public void setAccount(Account account) {
mAccount = account;
}
/** Sets the user handle used by the controller. */
public void setUserHandle(UserHandle userHandle) {
mUserHandle = userHandle;
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
/**
* Verifies that the controller was properly initialized with
* {@link #setAccount(Account)} and {@link #setUserHandle(UserHandle)}.
*
* @throws IllegalStateException if the account is {@code null}
*/
@Override
protected void checkInitialized() {
LOG.v("checkInitialized");
if (mAccount == null) {
throw new IllegalStateException(
"AccountSyncPreferenceController must be initialized by calling "
+ "setAccount(Account)");
}
if (mUserHandle == null) {
throw new IllegalStateException(
"AccountSyncPreferenceController must be initialized by calling "
+ "setUserHandle(UserHandle)");
}
}
@Override
protected void updateState(Preference preference) {
preference.setSummary(getSummary());
}
@Override
protected boolean handlePreferenceClicked(Preference preference) {
getFragmentController().launchFragment(
AccountSyncDetailsFragment.newInstance(mAccount, mUserHandle));
return true;
}
private CharSequence getSummary() {
int userId = mUserHandle.getIdentifier();
SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId);
int total = 0;
int enabled = 0;
if (syncAdapters != null) {
for (int i = 0, n = syncAdapters.length; i < n; i++) {
SyncAdapterType sa = syncAdapters[i];
// If the sync adapter isn't for this account type or if the user is not visible,
// don't show it.
if (!sa.accountType.equals(mAccount.type) || !sa.isUserVisible()) {
continue;
}
int syncState =
ContentResolver.getIsSyncableAsUser(mAccount, sa.authority, userId);
if (syncState > 0) {
// If the sync adapter is syncable, add it to the count of items that can be
// synced.
total++;
// If sync is enabled for the sync adapter at the master level or at the account
// level, add it to the count of items that are enabled.
boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(
mAccount, sa.authority, userId);
boolean oneTimeSyncMode =
!ContentResolver.getMasterSyncAutomaticallyAsUser(userId);
if (oneTimeSyncMode || syncEnabled) {
enabled++;
}
}
}
}
if (enabled == 0) {
return getContext().getText(R.string.account_sync_summary_all_off);
} else if (enabled == total) {
return getContext().getText(R.string.account_sync_summary_all_on);
} else {
return getContext().getString(R.string.account_sync_summary_some_on, enabled, total);
}
}
}

View File

@@ -0,0 +1,192 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorDescription;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import androidx.annotation.VisibleForTesting;
import com.android.settingslib.accounts.AuthenticatorHelper;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Utility that maintains a set of authorized account types.
*/
public class AccountTypesHelper {
/** Callback invoked when the set of authorized account types changes. */
public interface OnChangeListener {
/** Called when the set of authorized account types changes. */
void onAuthorizedAccountTypesChanged();
}
private final Context mContext;
private UserHandle mUserHandle;
private AuthenticatorHelper mAuthenticatorHelper;
private List<String> mAuthorities;
private Set<String> mAccountTypesFilter;
private Set<String> mAccountTypesExclusionFilter;
private Set<String> mAuthorizedAccountTypes;
private OnChangeListener mListener;
public AccountTypesHelper(Context context) {
mContext = context;
// Default to hardcoded Bluetooth account type.
mAccountTypesExclusionFilter = new HashSet<>();
mAccountTypesExclusionFilter.add("com.android.bluetooth.pbapsink");
setAccountTypesExclusionFilter(mAccountTypesExclusionFilter);
mUserHandle = UserHandle.of(UserHandle.myUserId());
mAuthenticatorHelper = new AuthenticatorHelper(mContext, mUserHandle,
userHandle -> {
// Only force a refresh if accounts have changed for the current user.
if (userHandle.equals(mUserHandle)) {
updateAuthorizedAccountTypes(false /* isForced */);
}
});
}
/** Sets the authorities that the user has. */
public void setAuthorities(List<String> authorities) {
mAuthorities = authorities;
}
/** Sets the filter for accounts that should be shown. */
public void setAccountTypesFilter(Set<String> accountTypesFilter) {
mAccountTypesFilter = accountTypesFilter;
}
/** Sets the filter for accounts that should NOT be shown. */
protected void setAccountTypesExclusionFilter(Set<String> accountTypesExclusionFilter) {
mAccountTypesExclusionFilter = accountTypesExclusionFilter;
}
/** Sets the callback invoked when the set of authorized account types changes. */
public void setOnChangeListener(OnChangeListener listener) {
mListener = listener;
}
/**
* Updates the set of authorized account types.
*
* <p>Derived from
* {@link com.android.settings.accounts.ChooseAccountActivity#onAuthDescriptionsUpdated}
*/
@VisibleForTesting
void updateAuthorizedAccountTypes(boolean isForced) {
AccountManager accountManager = AccountManager.get(mContext);
AuthenticatorDescription[] authenticatorDescriptions =
accountManager.getAuthenticatorTypesAsUser(mUserHandle.getIdentifier());
Set<String> authorizedAccountTypes = new HashSet<>();
for (AuthenticatorDescription authenticatorDescription : authenticatorDescriptions) {
String accountType = authenticatorDescription.type;
List<String> accountAuthorities =
mAuthenticatorHelper.getAuthoritiesForAccountType(accountType);
// If there are specific authorities required, we need to check whether they are
// included in the account type.
boolean authorized =
(mAuthorities == null || mAuthorities.isEmpty() || accountAuthorities == null
|| !Collections.disjoint(accountAuthorities, mAuthorities));
// If there is an account type filter, make sure this account type is included.
authorized = authorized && (mAccountTypesFilter == null
|| mAccountTypesFilter.contains(accountType));
// Check if the account type is in the exclusion list.
authorized = authorized && (mAccountTypesExclusionFilter == null
|| !mAccountTypesExclusionFilter.contains(accountType));
if (authorized) {
authorizedAccountTypes.add(accountType);
}
}
if (isForced || !Objects.equals(mAuthorizedAccountTypes, authorizedAccountTypes)) {
mAuthorizedAccountTypes = authorizedAccountTypes;
if (mListener != null) {
mListener.onAuthorizedAccountTypesChanged();
}
}
}
/** Returns the set of authorized account types, initializing the set first if necessary. */
public Set<String> getAuthorizedAccountTypes() {
if (mAuthorizedAccountTypes == null) {
updateAuthorizedAccountTypes(false /* isForced */);
}
return mAuthorizedAccountTypes;
}
/** Forces an update of the authorized account types. */
public void forceUpdate() {
updateAuthorizedAccountTypes(true /* isForced */);
}
/** Starts listening for account updates. */
public void listenToAccountUpdates() {
mAuthenticatorHelper.listenToAccountUpdates();
}
/** Stops listening for account updates. */
public void stopListeningToAccountUpdates() {
mAuthenticatorHelper.stopListeningToAccountUpdates();
}
/**
* Gets the label associated with a particular account type. Returns {@code null} if none found.
*
* @param accountType the type of account
*/
public CharSequence getLabelForType(String accountType) {
return mAuthenticatorHelper.getLabelForType(mContext, accountType);
}
/**
* Gets an icon associated with a particular account type. Returns a default icon if none found.
*
* @param accountType the type of account
* @return a drawable for the icon or a default icon returned by
* {@link PackageManager#getDefaultActivityIcon} if one cannot be found.
*/
public Drawable getDrawableForType(String accountType) {
return mAuthenticatorHelper.getDrawableForType(mContext, accountType);
}
/** Used for testing to trigger an account update. */
@VisibleForTesting
AuthenticatorHelper getAuthenticatorHelper() {
return mAuthenticatorHelper;
}
@VisibleForTesting
void setAuthenticatorHelper(AuthenticatorHelper helper) {
mAuthenticatorHelper = helper;
}
}

View File

@@ -0,0 +1,207 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.car.settings.accounts;
import static android.content.Intent.EXTRA_USER;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.widget.Toast;
import com.android.car.settings.R;
import com.android.car.settings.common.Logger;
import java.io.IOException;
/**
* Entry point Activity for account setup. Works as follows
*
* <ol>
* <li> After receiving an account type from ChooseAccountFragment, this Activity launches the
* account setup specified by AccountManager.
* <li> After the account setup, this Activity finishes without showing anything.
* </ol>
*/
public class AddAccountActivity extends Activity {
/**
* A boolean to keep the state of whether add account has already been called.
*/
private static final String KEY_ADD_CALLED = "AddAccountCalled";
/**
* Extra parameter to identify the caller. Applications may display a
* different UI if the call is made from Settings or from a specific
* application.
*/
private static final String KEY_CALLER_IDENTITY = "pendingIntent";
private static final String SHOULD_NOT_RESOLVE = "SHOULDN'T RESOLVE!";
private static final Logger LOG = new Logger(AddAccountActivity.class);
private static final String ALLOW_SKIP = "allowSkip";
/* package */ static final String EXTRA_SELECTED_ACCOUNT = "selected_account";
// Show additional info regarding the use of a device with multiple users
static final String EXTRA_HAS_MULTIPLE_USERS = "hasMultipleUsers";
// Need a specific request code for add account activity.
private static final int ADD_ACCOUNT_REQUEST = 2001;
private UserManager mUserManager;
private UserHandle mUserHandle;
private PendingIntent mPendingIntent;
private boolean mAddAccountCalled;
private final AccountManagerCallback<Bundle> mCallback = new AccountManagerCallback<Bundle>() {
@Override
public void run(AccountManagerFuture<Bundle> future) {
if (!future.isDone()) {
LOG.v("Account manager future is not done.");
finish();
}
boolean done = true;
try {
Bundle result = future.getResult();
Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
if (intent != null) {
done = false;
Bundle addAccountOptions = new Bundle();
addAccountOptions.putBoolean(EXTRA_HAS_MULTIPLE_USERS,
hasMultipleUsers(AddAccountActivity.this));
addAccountOptions.putParcelable(EXTRA_USER, mUserHandle);
intent.putExtras(addAccountOptions);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivityForResultAsUser(
new Intent(intent), ADD_ACCOUNT_REQUEST, mUserHandle);
} else {
setResult(RESULT_OK);
if (mPendingIntent != null) {
mPendingIntent.cancel();
mPendingIntent = null;
}
}
LOG.v("account added: " + result);
} catch (OperationCanceledException | IOException | AuthenticatorException e) {
LOG.v("addAccount error: " + e);
} finally {
if (done) {
finish();
}
}
}
};
/**
* Creates an intent to start the {@link AddAccountActivity} to add an account of the given
* account type.
*/
public static Intent createAddAccountActivityIntent(Context context, String accountType) {
Intent intent = new Intent(context, AddAccountActivity.class);
intent.putExtra(EXTRA_SELECTED_ACCOUNT, accountType);
return intent;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_ADD_CALLED, mAddAccountCalled);
LOG.v("saved");
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mAddAccountCalled = savedInstanceState.getBoolean(KEY_ADD_CALLED);
LOG.v("Restored from previous add account call: " + mAddAccountCalled);
}
mUserManager = UserManager.get(getApplicationContext());
mUserHandle = UserHandle.of(UserHandle.myUserId());
if (mUserManager.hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS)) {
// We aren't allowed to add an account.
Toast.makeText(
this, R.string.user_cannot_add_accounts_message, Toast.LENGTH_LONG)
.show();
finish();
return;
}
if (mAddAccountCalled) {
// We already called add account - maybe the callback was lost.
finish();
return;
}
addAccount(getIntent().getStringExtra(EXTRA_SELECTED_ACCOUNT));
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
setResult(resultCode);
if (mPendingIntent != null) {
mPendingIntent.cancel();
mPendingIntent = null;
}
finish();
}
private void addAccount(String accountType) {
Bundle addAccountOptions = new Bundle();
addAccountOptions.putBoolean(EXTRA_HAS_MULTIPLE_USERS, hasMultipleUsers(this));
addAccountOptions.putBoolean(ALLOW_SKIP, true);
/*
* The identityIntent is for the purposes of establishing the identity
* of the caller and isn't intended for launching activities, services
* or broadcasts.
*/
Intent identityIntent = new Intent();
identityIntent.setComponent(new ComponentName(SHOULD_NOT_RESOLVE, SHOULD_NOT_RESOLVE));
identityIntent.setAction(SHOULD_NOT_RESOLVE);
identityIntent.addCategory(SHOULD_NOT_RESOLVE);
mPendingIntent =
PendingIntent.getBroadcast(this, 0, identityIntent, PendingIntent.FLAG_IMMUTABLE);
addAccountOptions.putParcelable(KEY_CALLER_IDENTITY, mPendingIntent);
AccountManager.get(this).addAccountAsUser(
accountType,
/* authTokenType= */ null,
/* requiredFeatures= */ null,
addAccountOptions,
null,
mCallback,
/* handler= */ null,
mUserHandle);
mAddAccountCalled = true;
}
private boolean hasMultipleUsers(Context context) {
return ((UserManager) context.getSystemService(Context.USER_SERVICE))
.getUsers().size() > 1;
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import static android.app.Activity.RESULT_OK;
import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS;
import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm;
import static com.android.car.settings.enterprise.EnterpriseUtils.isDeviceManaged;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.car.settings.admin.NewUserDisclaimerActivity;
import com.android.car.settings.common.ActivityResultCallback;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.PreferenceController;
import com.android.car.settings.profiles.ProfileHelper;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Business Logic for preference starts the add account flow.
*/
public class AddAccountPreferenceController extends PreferenceController<Preference>
implements ActivityResultCallback {
public static final int NEW_USER_DISCLAIMER_REQUEST = 1;
private static final Logger LOG = new Logger(AddAccountPreferenceController.class);
private String[] mAuthorities;
private String[] mAccountTypes;
public AddAccountPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
/** Sets the account authorities that are available. */
public AddAccountPreferenceController setAuthorities(String[] authorities) {
mAuthorities = authorities;
return this;
}
/** Sets the account authorities that are available. */
public AddAccountPreferenceController setAccountTypes(String[] accountTypes) {
mAccountTypes = accountTypes;
return this;
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
@Override
protected void onCreateInternal() {
super.onCreateInternal();
setClickableWhileDisabled(getPreference(), /* clickable= */ true, p -> {
if (!getProfileHelper().isNewUserDisclaimerAcknolwedged(getContext())) {
LOG.d("isNewUserDisclaimerAcknolwedged returns false, start activity");
startNewUserDisclaimerActivityForResult();
return;
}
getProfileHelper()
.runClickableWhileDisabled(getContext(), getFragmentController());
});
}
@Override
protected boolean handlePreferenceClicked(Preference preference) {
AccountTypesHelper helper = getAccountTypesHelper();
if (mAuthorities != null) {
helper.setAuthorities(Arrays.asList(mAuthorities));
}
if (mAccountTypes != null) {
helper.setAccountTypesFilter(
new HashSet<>(Arrays.asList(mAccountTypes)));
}
launchAddAccount();
return true;
}
@Override
public void processActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == NEW_USER_DISCLAIMER_REQUEST && resultCode == RESULT_OK) {
LOG.d("New user disclaimer acknowledged, launching add account.");
launchAddAccount();
} else {
LOG.e("New user disclaimer failed with result " + resultCode);
}
}
private void launchAddAccount() {
Set<String> authorizedAccountTypes = getAccountTypesHelper().getAuthorizedAccountTypes();
// Skip the choose account screen if there is only one account type and the device is not
// managed by a device owner.
// TODO (b/213894991) add check for profile owner when work profile is supported
if (authorizedAccountTypes.size() == 1 && !isDeviceManaged(getContext())) {
String accountType = authorizedAccountTypes.iterator().next();
getContext().startActivity(
AddAccountActivity.createAddAccountActivityIntent(getContext(), accountType));
} else {
getFragmentController().launchFragment(new ChooseAccountFragment());
}
}
@Override
protected int getDefaultAvailabilityStatus() {
if (isDeviceManaged(getContext())
&& !getProfileHelper().isNewUserDisclaimerAcknolwedged(getContext())) {
return AVAILABLE_FOR_VIEWING;
}
if (getProfileHelper().canCurrentProcessModifyAccounts()) {
return AVAILABLE;
}
if (getProfileHelper().isDemoOrGuest()
|| hasUserRestrictionByUm(getContext(), DISALLOW_MODIFY_ACCOUNTS)) {
return DISABLED_FOR_PROFILE;
}
return AVAILABLE_FOR_VIEWING;
}
private void startNewUserDisclaimerActivityForResult() {
getFragmentController().startActivityForResult(
new Intent(getContext(), NewUserDisclaimerActivity.class),
NEW_USER_DISCLAIMER_REQUEST, /* callback= */ this);
}
@VisibleForTesting
ProfileHelper getProfileHelper() {
return ProfileHelper.getInstance(getContext());
}
@VisibleForTesting
AccountTypesHelper getAccountTypesHelper() {
return new AccountTypesHelper(getContext());
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import android.content.Context;
import android.content.Intent;
import android.provider.Settings;
import androidx.annotation.XmlRes;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
import java.util.Arrays;
import java.util.HashSet;
/**
* Lists accounts the user can add.
*/
public class ChooseAccountFragment extends SettingsFragment {
@Override
@XmlRes
protected int getPreferenceScreenResId() {
return R.xml.choose_account_fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
Intent intent = requireActivity().getIntent();
ChooseAccountPreferenceController preferenceController = use(
ChooseAccountPreferenceController.class, R.string.pk_add_account);
String[] authorities = intent.getStringArrayExtra(Settings.EXTRA_AUTHORITIES);
if (authorities != null) {
preferenceController.setAuthorities(Arrays.asList(authorities));
}
String[] accountTypesForFilter = intent.getStringArrayExtra(Settings.EXTRA_ACCOUNT_TYPES);
if (accountTypesForFilter != null) {
preferenceController.setAccountTypesFilter(
new HashSet<>(Arrays.asList(accountTypesForFilter)));
}
}
}

View File

@@ -0,0 +1,218 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.collection.ArrayMap;
import androidx.preference.PreferenceGroup;
import com.android.car.settings.common.ActivityResultCallback;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.car.ui.preference.CarUiPreference;
import com.android.settingslib.accounts.AuthenticatorHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Controller for showing the user the list of accounts they can add.
*
* <p>Largely derived from {@link com.android.settings.accounts.ChooseAccountActivity}
*/
public class ChooseAccountPreferenceController extends
PreferenceController<PreferenceGroup> implements ActivityResultCallback {
@VisibleForTesting
static final int ADD_ACCOUNT_REQUEST_CODE = 100;
private AccountTypesHelper mAccountTypesHelper;
private ArrayMap<String, AuthenticatorDescriptionPreference> mPreferences = new ArrayMap<>();
private boolean mHasPendingBack = false;
public ChooseAccountPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mAccountTypesHelper = new AccountTypesHelper(context);
mAccountTypesHelper.setOnChangeListener(this::forceUpdateAccountsCategory);
}
/** Sets the authorities that the user has. */
public void setAuthorities(List<String> authorities) {
mAccountTypesHelper.setAuthorities(authorities);
}
/** Sets the filter for accounts that should be shown. */
public void setAccountTypesFilter(Set<String> accountTypesFilter) {
mAccountTypesHelper.setAccountTypesFilter(accountTypesFilter);
}
/** Sets the filter for accounts that should NOT be shown. */
protected void setAccountTypesExclusionFilter(Set<String> accountTypesExclusionFilterFilter) {
mAccountTypesHelper.setAccountTypesExclusionFilter(accountTypesExclusionFilterFilter);
}
@Override
protected Class<PreferenceGroup> getPreferenceType() {
return PreferenceGroup.class;
}
@Override
protected void updateState(PreferenceGroup preferenceGroup) {
mAccountTypesHelper.forceUpdate();
}
/**
* Registers the account update callback.
*/
@Override
protected void onStartInternal() {
mAccountTypesHelper.listenToAccountUpdates();
if (mHasPendingBack) {
mHasPendingBack = false;
// Post the fragment navigation because FragmentManager may still be executing
// transactions during onStart.
new Handler().post(() -> getFragmentController().goBack());
}
}
/**
* Unregisters the account update callback.
*/
@Override
protected void onStopInternal() {
mAccountTypesHelper.stopListeningToAccountUpdates();
}
/** Forces a refresh of the authenticator description preferences. */
private void forceUpdateAccountsCategory() {
Set<String> preferencesToRemove = new HashSet<>(mPreferences.keySet());
List<AuthenticatorDescriptionPreference> preferences =
getAuthenticatorDescriptionPreferences(preferencesToRemove);
// Add all preferences that aren't already shown
for (int i = 0; i < preferences.size(); i++) {
AuthenticatorDescriptionPreference preference = preferences.get(i);
preference.setOrder(i);
String key = preference.getKey();
getPreference().addPreference(preference);
mPreferences.put(key, preference);
}
// Remove all preferences that should no longer be shown
for (String key : preferencesToRemove) {
getPreference().removePreference(mPreferences.get(key));
mPreferences.remove(key);
}
}
/**
* Returns a list of preferences corresponding to the account types the user can add.
*
* <p> Derived from
* {@link com.android.settings.accounts.ChooseAccountActivity#onAuthDescriptionsUpdated}
*
* @param preferencesToRemove the current preferences shown; will contain the preferences that
* need to be removed from the screen after method execution
*/
private List<AuthenticatorDescriptionPreference> getAuthenticatorDescriptionPreferences(
Set<String> preferencesToRemove) {
ArrayList<AuthenticatorDescriptionPreference> authenticatorDescriptionPreferences =
new ArrayList<>();
Set<String> authorizedAccountTypes = mAccountTypesHelper.getAuthorizedAccountTypes();
// Create list of account providers to show on page.
for (String accountType : authorizedAccountTypes) {
CharSequence label = mAccountTypesHelper.getLabelForType(accountType);
Drawable icon = mAccountTypesHelper.getDrawableForType(accountType);
// Add a preference for the provider to the list and remove it from preferencesToRemove.
AuthenticatorDescriptionPreference preference = mPreferences.getOrDefault(accountType,
new AuthenticatorDescriptionPreference(getContext(), accountType, label, icon));
preference.setOnPreferenceClickListener(
pref -> {
Intent intent = AddAccountActivity.createAddAccountActivityIntent(
getContext(), preference.getAccountType());
getFragmentController().startActivityForResult(intent,
ADD_ACCOUNT_REQUEST_CODE, /* callback= */ this);
return true;
});
authenticatorDescriptionPreferences.add(preference);
preferencesToRemove.remove(accountType);
}
Collections.sort(authenticatorDescriptionPreferences);
return authenticatorDescriptionPreferences;
}
/** Used for testing to trigger an account update. */
@VisibleForTesting
AccountTypesHelper getAccountTypesHelper() {
return mAccountTypesHelper;
}
@VisibleForTesting
void setAuthenticatorHelper(AuthenticatorHelper helper) {
mAccountTypesHelper.setAuthenticatorHelper(helper);
}
@Override
public void processActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == ADD_ACCOUNT_REQUEST_CODE) {
if (isStarted()) {
getFragmentController().goBack();
} else {
mHasPendingBack = true;
}
}
}
/** Handles adding accounts. */
interface AddAccountListener {
/** Handles adding an account. */
void addAccount(String accountType);
}
private static class AuthenticatorDescriptionPreference extends CarUiPreference {
private final String mType;
AuthenticatorDescriptionPreference(Context context, String accountType, CharSequence label,
Drawable icon) {
super(context);
mType = accountType;
setKey(accountType);
setTitle(label);
setIcon(icon);
setShowChevron(false);
}
private String getAccountType() {
return mType;
}
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.accounts;
import android.content.Context;
import android.view.View;
import android.widget.TextView;
import androidx.preference.PreferenceViewHolder;
import com.android.car.settings.R;
import com.android.car.ui.preference.CarUiSwitchPreference;
/**
* A preference that represents the state of a sync adapter.
*
* <p>Largely derived from {@link com.android.settings.accounts.SyncStateSwitchPreference}.
*/
public class SyncPreference extends CarUiSwitchPreference {
private int mUid;
private String mPackageName;
private AccountSyncHelper.SyncState mSyncState = AccountSyncHelper.SyncState.NONE;
private boolean mOneTimeSyncMode = false;
public SyncPreference(Context context, String authority) {
super(context);
setKey(authority);
setPersistent(false);
updateIcon();
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
View switchView = view.findViewById(com.android.internal.R.id.switch_widget);
if (mOneTimeSyncMode) {
switchView.setVisibility(View.GONE);
/*
* Override the summary. Fill in the %1$s with the existing summary
* (what ends up happening is the old summary is shown on the next
* line).
*/
TextView summary = (TextView) view.findViewById(android.R.id.summary);
summary.setText(getContext().getString(R.string.sync_one_time_sync, getSummary()));
} else {
switchView.setVisibility(View.VISIBLE);
}
}
/** Updates the preference icon based on the current syncing state. */
private void updateIcon() {
switch (mSyncState) {
case ACTIVE:
setIcon(R.drawable.ic_sync_anim);
getIcon().setTintList(getContext().getColorStateList(R.color.icon_color_default));
break;
case PENDING:
setIcon(R.drawable.ic_sync);
getIcon().setTintList(getContext().getColorStateList(R.color.icon_color_default));
break;
case FAILED:
setIcon(R.drawable.ic_sync_problem);
getIcon().setTintList(getContext().getColorStateList(R.color.icon_color_default));
break;
default:
setIcon(null);
setIconSpaceReserved(true);
break;
}
}
/** Sets the sync state for this preference. */
public void setSyncState(AccountSyncHelper.SyncState state) {
mSyncState = state;
// Force a manual update of the icon since the sync state affects what is shown.
updateIcon();
}
/**
* Returns whether or not the sync adapter is in one-time sync mode.
*
* <p>One-time sync mode means that the sync adapter is not being automatically synced but
* can be manually synced (i.e. a one time sync).
*/
public boolean isOneTimeSyncMode() {
return mOneTimeSyncMode;
}
/** Sets whether one-time sync mode is on for this preference. */
public void setOneTimeSyncMode(boolean oneTimeSyncMode) {
mOneTimeSyncMode = oneTimeSyncMode;
// Force a refresh so that onBindViewHolder is called
notifyChanged();
}
/**
* Returns the uid corresponding to the sync adapter's package.
*
* <p>This can be used to create an intent to request account access via
* {@link android.accounts.AccountManager#createRequestAccountAccessIntentSenderAsUser}.
*/
public int getUid() {
return mUid;
}
/** Sets the uid for this preference. */
public void setUid(int uid) {
mUid = uid;
}
public String getPackageName() {
return mPackageName;
}
public void setPackageName(String packageName) {
mPackageName = packageName;
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.admin;
import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_PARKED;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.car.Car;
import android.car.ICarResultReceiver;
import android.car.drivingstate.CarDrivingStateEvent;
import android.car.drivingstate.CarDrivingStateManager;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.widget.Toast;
import com.android.car.settings.R;
import com.android.car.settings.common.Logger;
/**
* Activity shown when a factory request is imminent, it gives the user the option to reset now or
* wait until the device is rebooted / resumed from suspend.
*/
public final class FactoryResetActivity extends Activity {
private static final String EXTRA_FACTORY_RESET_CALLBACK = "factory_reset_callback";
private static final int FACTORY_RESET_NOTIFICATION_ID = 42;
private static final Logger LOG = new Logger(FactoryResetActivity.class);
private ICarResultReceiver mCallback;
private Car mCar;
private CarDrivingStateManager mCarDrivingStateManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
Object binder = null;
try {
binder = intent.getExtra(EXTRA_FACTORY_RESET_CALLBACK);
mCallback = ICarResultReceiver.Stub.asInterface((IBinder) binder);
} catch (Exception e) {
LOG.w("error getting IResultReveiver from " + EXTRA_FACTORY_RESET_CALLBACK
+ " extra (" + binder + ") on " + intent, e);
}
if (mCallback == null) {
LOG.wtf("no ICarResultReceiver / " + EXTRA_FACTORY_RESET_CALLBACK
+ " extra on " + intent);
finish();
return;
}
// Connect to car service
mCar = Car.createCar(this);
mCarDrivingStateManager = (CarDrivingStateManager) mCar.getCarManager(
Car.CAR_DRIVING_STATE_SERVICE);
showMore();
}
@Override
protected void onStop() {
super.onStop();
finish();
}
private void showMore() {
CarDrivingStateEvent state = mCarDrivingStateManager.getCurrentCarDrivingState();
switch (state.eventValue) {
case DRIVING_STATE_PARKED:
showFactoryResetDialog();
break;
default:
showFactoryResetToast();
}
}
private void showFactoryResetDialog() {
AlertDialog dialog = new AlertDialog.Builder(/* context= */ this,
com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert)
.setTitle(R.string.factory_reset_parked_title)
.setMessage(R.string.factory_reset_parked_text)
.setPositiveButton(R.string.factory_reset_later_button,
(d, which) -> factoryResetLater())
.setNegativeButton(R.string.factory_reset_now_button,
(d, which) -> factoryResetNow())
.setCancelable(false)
.setOnDismissListener((d) -> finish())
.create();
dialog.show();
}
private void showFactoryResetToast() {
showToast(R.string.factory_reset_driving_text);
finish();
}
private void factoryResetNow() {
LOG.i("Factory reset acknowledged; finishing it");
try {
mCallback.send(/* resultCode= */ 0, /* resultData= */ null);
// Cancel pending intent and notification
getSystemService(NotificationManager.class).cancel(FACTORY_RESET_NOTIFICATION_ID);
PendingIntent.getActivity(this, FACTORY_RESET_NOTIFICATION_ID, getIntent(),
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT).cancel();
} catch (Exception e) {
LOG.e("error factory resetting or cancelling notification / intent", e);
return;
} finally {
finish();
}
}
private void factoryResetLater() {
LOG.i("Delaying factory reset.");
showToast(R.string.factory_reset_later_text);
finish();
}
private void showToast(int resId) {
Toast.makeText(this, resId, Toast.LENGTH_LONG).show();
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.admin;
import android.car.Car;
import android.car.admin.CarDevicePolicyManager;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import android.widget.Button;
import androidx.fragment.app.FragmentActivity;
import com.android.car.admin.ui.ManagedDeviceTextView;
import com.android.car.settings.R;
import com.android.car.settings.common.ConfirmationDialogFragment;
import com.android.car.settings.common.Logger;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin;
/**
* Shows a disclaimer dialog when a new user is added in a device that is managed by a device owner.
*
* <p>The dialog text will contain the message from
* {@code ManagedDeviceTextView.getManagedDeviceText}.
*
* <p>The dialog contains two buttons: one to acknowlege the disclaimer; the other to launch
* {@code Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS} for more details. Note: when
* {@code Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS} is closed, the same dialog will be shown.
*
* <p>Clicking anywhere outside the dialog will dimiss the dialog.
*/
public final class NewUserDisclaimerActivity extends FragmentActivity {
@VisibleForTesting
static final Logger LOG = new Logger(NewUserDisclaimerActivity.class);
@VisibleForTesting
static final String DIALOG_TAG = "NewUserDisclaimerActivity.ConfirmationDialogFragment";
private static final int LEARN_MORE_RESULT_CODE = 1;
private Car mCar;
private CarDevicePolicyManager mCarDevicePolicyManager;
private Button mAcceptButton;
private ConfirmationDialogFragment mConfirmationDialog;
private boolean mLearnMoreLaunched;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLifecycle().addObserver(new HideNonSystemOverlayMixin(this));
getCarDevicePolicyManager();
setupConfirmationDialog();
}
@Override
protected void onResume() {
super.onResume();
showConfirmationDialog();
getCarDevicePolicyManager().setUserDisclaimerShown(getUser());
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != LEARN_MORE_RESULT_CODE) {
LOG.w("onActivityResult(), invalid request code: " + requestCode);
return;
}
mLearnMoreLaunched = false;
}
private ConfirmationDialogFragment setupConfirmationDialog() {
String managedByOrganizationText = ManagedDeviceTextView.getManagedDeviceText(this)
.toString();
String managedProfileText = getResources().getString(
R.string.new_user_managed_device_text);
mConfirmationDialog = new ConfirmationDialogFragment.Builder(getApplicationContext())
.setTitle(R.string.new_user_managed_device_title)
.setMessage(managedByOrganizationText + System.lineSeparator()
+ System.lineSeparator() + managedProfileText)
.setPositiveButton(R.string.new_user_managed_device_acceptance,
arguments -> onAccept())
.setNeutralButton(R.string.new_user_managed_device_learn_more,
arguments -> onLearnMoreClicked())
.setDismissListener((arguments, positiveResult) -> onDialogDimissed())
.build();
return mConfirmationDialog;
}
private void showConfirmationDialog() {
if (mConfirmationDialog == null) {
setupConfirmationDialog();
}
mConfirmationDialog.show(getSupportFragmentManager(), DIALOG_TAG);
}
private void onAccept() {
LOG.d("user accepted");
getCarDevicePolicyManager().setUserDisclaimerAcknowledged(getUser());
setResult(RESULT_OK);
finish();
}
private void onLearnMoreClicked() {
mLearnMoreLaunched = true;
startActivityForResult(new Intent(Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS),
LEARN_MORE_RESULT_CODE);
}
private void onDialogDimissed() {
if (mLearnMoreLaunched) {
return;
}
finish();
}
private CarDevicePolicyManager getCarDevicePolicyManager() {
LOG.d("getCarDevicePolicyManager for user: " + getUser());
if (mCarDevicePolicyManager != null) {
return mCarDevicePolicyManager;
}
if (mCar == null) {
mCar = Car.createCar(this);
}
mCarDevicePolicyManager = (CarDevicePolicyManager) mCar.getCarManager(
Car.CAR_DEVICE_POLICY_SERVICE);
return mCarDevicePolicyManager;
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.app.usage.UsageStats;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import java.util.List;
/**
* Controller that shows when there is no recently used apps.
* Sets availability based on if there are recent apps and sets summary depending on number of
* non-system apps.
*/
public class AllAppsPreferenceController extends PreferenceController<Preference> implements
RecentAppsItemManager.RecentAppStatsListener,
InstalledAppCountItemManager.InstalledAppCountListener {
// In most cases, device has recently opened apps. So, assume true by default.
private boolean mAreThereRecentlyUsedApps = true;
public AllAppsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
@Override
public int getDefaultAvailabilityStatus() {
return mAreThereRecentlyUsedApps ? CONDITIONALLY_UNAVAILABLE : AVAILABLE;
}
@Override
public void onRecentAppStatsLoaded(List<UsageStats> recentAppStats) {
mAreThereRecentlyUsedApps = !recentAppStats.isEmpty();
refreshUi();
}
@Override
public void onInstalledAppCountLoaded(int appCount) {
getPreference().setSummary(getContext().getResources().getString(
R.string.apps_view_all_apps_title, appCount));
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import com.android.car.settings.common.SettingsFragment;
/**
* Fragment base class for use in cases where a list of applications is displayed. Includes a
* a shared preference instance that can be used to show/hide system apps in the list.
*/
public abstract class AppListFragment extends SettingsFragment {
protected static final String SHARED_PREFERENCE_PATH =
"com.android.car.settings.applications.AppListFragment";
protected static final String KEY_HIDE_SYSTEM =
"com.android.car.settings.applications.HIDE_SYSTEM";
private boolean mHideSystem = true;
private SharedPreferences mSharedPreferences;
private SharedPreferences.OnSharedPreferenceChangeListener mSharedPreferenceChangeListener =
(sharedPreferences, key) -> {
if (KEY_HIDE_SYSTEM.equals(key)) {
mHideSystem = sharedPreferences.getBoolean(KEY_HIDE_SYSTEM,
/* defaultValue= */ true);
onToggleShowSystemApps(!mHideSystem);
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSharedPreferences =
getContext().getSharedPreferences(SHARED_PREFERENCE_PATH, Context.MODE_PRIVATE);
if (savedInstanceState != null) {
mHideSystem = savedInstanceState.getBoolean(KEY_HIDE_SYSTEM,
/* defaultValue= */ true);
mSharedPreferences.edit().putBoolean(KEY_HIDE_SYSTEM, mHideSystem).apply();
} else {
mSharedPreferences.edit().putBoolean(KEY_HIDE_SYSTEM, true).apply();
}
}
@Override
public void onStart() {
super.onStart();
onToggleShowSystemApps(!mHideSystem);
mSharedPreferences.registerOnSharedPreferenceChangeListener(
mSharedPreferenceChangeListener);
}
@Override
public void onStop() {
super.onStop();
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(
mSharedPreferenceChangeListener);
}
/** Called when a user toggles the option to show system applications in the list. */
protected abstract void onToggleShowSystemApps(boolean showSystem);
/** Returns {@code true} if system applications should be shown in the list. */
protected final boolean shouldShowSystemApps() {
return !mHideSystem;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_HIDE_SYSTEM, mHideSystem);
}
}

View File

@@ -0,0 +1,580 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import static android.app.Activity.RESULT_FIRST_USER;
import static android.app.Activity.RESULT_OK;
import static com.android.car.settings.applications.ApplicationsUtils.isKeepEnabledPackage;
import static com.android.car.settings.applications.ApplicationsUtils.isProfileOrDeviceOwner;
import static com.android.car.settings.common.ActionButtonsPreference.ActionButtons;
import static com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment.DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG;
import static com.android.car.settings.enterprise.EnterpriseUtils.BLOCKED_UNINSTALL_APP;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.admin.DevicePolicyManager;
import android.car.drivingstate.CarUxRestrictions;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArraySet;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.car.settings.R;
import com.android.car.settings.common.ActionButtonInfo;
import com.android.car.settings.common.ActionButtonsPreference;
import com.android.car.settings.common.ActivityResultCallback;
import com.android.car.settings.common.ConfirmationDialogFragment;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.PreferenceController;
import com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment;
import com.android.car.settings.enterprise.DeviceAdminAddActivity;
import com.android.car.settings.enterprise.EnterpriseUtils;
import com.android.car.settings.profiles.ProfileHelper;
import com.android.settingslib.Utils;
import com.android.settingslib.applications.ApplicationsState;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/**
* Shows actions associated with an application, like uninstall and forceStop.
*
* <p>To uninstall an app, it must <i>not</i> be:
* <ul>
* <li>a system bundled app
* <li>system signed
* <li>managed by an active admin from a device policy
* <li>a device or profile owner
* <li>the only home app
* <li>the default home app
* <li>for a user with the {@link UserManager#DISALLOW_APPS_CONTROL} restriction
* <li>for a user with the {@link UserManager#DISALLOW_UNINSTALL_APPS} restriction
* </ul>
*
* <p>For apps that cannot be uninstalled, a disable option is shown instead (or enable if the app
* is already disabled).
*/
public class ApplicationActionButtonsPreferenceController extends
PreferenceController<ActionButtonsPreference> implements ActivityResultCallback {
private static final Logger LOG = new Logger(
ApplicationActionButtonsPreferenceController.class);
private static final List<String> FORCE_STOP_RESTRICTIONS =
Arrays.asList(UserManager.DISALLOW_APPS_CONTROL);
private static final List<String> UNINSTALL_RESTRICTIONS =
Arrays.asList(UserManager.DISALLOW_UNINSTALL_APPS, UserManager.DISALLOW_APPS_CONTROL);
private static final List<String> DISABLE_RESTRICTIONS =
Arrays.asList(UserManager.DISALLOW_APPS_CONTROL);
@VisibleForTesting
static final String DISABLE_CONFIRM_DIALOG_TAG =
"com.android.car.settings.applications.DisableConfirmDialog";
@VisibleForTesting
static final String FORCE_STOP_CONFIRM_DIALOG_TAG =
"com.android.car.settings.applications.ForceStopConfirmDialog";
@VisibleForTesting
static final int UNINSTALL_REQUEST_CODE = 10;
@VisibleForTesting
static final int UNINSTALL_DEVICE_ADMIN_REQUEST_CODE = 11;
private DevicePolicyManager mDpm;
private PackageManager mPm;
private UserManager mUserManager;
private ProfileHelper mProfileHelper;
private ApplicationsState.Session mSession;
private ApplicationsState.AppEntry mAppEntry;
private ApplicationsState mApplicationsState;
private String mPackageName;
private PackageInfo mPackageInfo;
private String mRestriction;
@VisibleForTesting
final ConfirmationDialogFragment.ConfirmListener mForceStopConfirmListener =
new ConfirmationDialogFragment.ConfirmListener() {
@Override
public void onConfirm(@Nullable Bundle arguments) {
LOG.d("Stopping package " + mPackageName);
getContext().getSystemService(ActivityManager.class)
.forceStopPackage(mPackageName);
int userId = UserHandle.getUserId(mAppEntry.info.uid);
mApplicationsState.invalidatePackage(mPackageName, userId);
Toast.makeText(getContext(), getContext().getResources()
.getString(R.string.force_stop_success_toast_text,
mAppEntry.info.loadLabel(mPm)), Toast.LENGTH_LONG).show();
}
};
private final View.OnClickListener mForceStopClickListener = i -> {
if (ignoreActionBecauseItsDisabledByAdmin(FORCE_STOP_RESTRICTIONS)) return;
ConfirmationDialogFragment dialogFragment =
new ConfirmationDialogFragment.Builder(getContext())
.setTitle(R.string.force_stop_dialog_title)
.setMessage(R.string.force_stop_dialog_text)
.setPositiveButton(android.R.string.ok,
mForceStopConfirmListener)
.setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
.build();
getFragmentController().showDialog(dialogFragment, FORCE_STOP_CONFIRM_DIALOG_TAG);
};
@VisibleForTesting
final BroadcastReceiver mCheckKillProcessesReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
boolean enabled = getResultCode() != Activity.RESULT_CANCELED;
LOG.d("Got broadcast response: Restart status for " + mPackageName + " " + enabled);
updateForceStopButtonInner(enabled);
}
};
@VisibleForTesting
final ConfirmationDialogFragment.ConfirmListener mDisableConfirmListener = i -> {
mPm.setApplicationEnabledSetting(mPackageName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, /* flags= */ 0);
updateUninstallButtonInner(false);
};
private final View.OnClickListener mDisableClickListener = i -> {
if (ignoreActionBecauseItsDisabledByAdmin(DISABLE_RESTRICTIONS)) return;
ConfirmationDialogFragment dialogFragment =
new ConfirmationDialogFragment.Builder(getContext())
.setMessage(getContext().getString(R.string.app_disable_dialog_text))
.setPositiveButton(R.string.app_disable_dialog_positive,
mDisableConfirmListener)
.setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
.build();
getFragmentController().showDialog(dialogFragment, DISABLE_CONFIRM_DIALOG_TAG);
};
private final View.OnClickListener mEnableClickListener = i -> {
if (ignoreActionBecauseItsDisabledByAdmin(DISABLE_RESTRICTIONS)) return;
mPm.setApplicationEnabledSetting(mPackageName,
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, /* flags= */ 0);
updateUninstallButtonInner(true);
};
private final View.OnClickListener mUninstallClickListener = i -> {
if (ignoreActionBecauseItsDisabledByAdmin(UNINSTALL_RESTRICTIONS)) return;
Uri packageUri = Uri.parse("package:" + mPackageName);
if (mDpm.packageHasActiveAdmins(mPackageName)) {
// Show Device Admin app details screen to deactivate the device admin before it can
// be uninstalled.
Intent deviceAdminIntent = new Intent(getContext(), DeviceAdminAddActivity.class)
.putExtra(DeviceAdminAddActivity.EXTRA_DEVICE_ADMIN_PACKAGE_NAME, mPackageName);
getFragmentController().startActivityForResult(deviceAdminIntent,
UNINSTALL_DEVICE_ADMIN_REQUEST_CODE, /* callback= */ this);
} else {
Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri);
uninstallIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
getFragmentController().startActivityForResult(uninstallIntent, UNINSTALL_REQUEST_CODE,
/* callback= */ this);
}
};
private final ApplicationsState.Callbacks mApplicationStateCallbacks =
new ApplicationsState.Callbacks() {
@Override
public void onRunningStateChanged(boolean running) {
}
@Override
public void onPackageListChanged() {
refreshUi();
}
@Override
public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
}
@Override
public void onPackageIconChanged() {
}
@Override
public void onPackageSizeChanged(String packageName) {
}
@Override
public void onAllSizesComputed() {
}
@Override
public void onLauncherInfoChanged() {
}
@Override
public void onLoadEntriesCompleted() {
}
};
public ApplicationActionButtonsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mDpm = context.getSystemService(DevicePolicyManager.class);
mPm = context.getPackageManager();
mUserManager = UserManager.get(context);
mProfileHelper = ProfileHelper.getInstance(context);
}
@Override
protected Class<ActionButtonsPreference> getPreferenceType() {
return ActionButtonsPreference.class;
}
/** Sets the {@link ApplicationsState.AppEntry} which is used to load the app name and icon. */
public ApplicationActionButtonsPreferenceController setAppEntry(
ApplicationsState.AppEntry appEntry) {
mAppEntry = appEntry;
return this;
}
/** Sets the {@link ApplicationsState} which is used to load the app name and icon. */
public ApplicationActionButtonsPreferenceController setAppState(
ApplicationsState applicationsState) {
mApplicationsState = applicationsState;
return this;
}
/**
* Set the packageName, which is used to perform actions on a particular package.
*/
public ApplicationActionButtonsPreferenceController setPackageName(String packageName) {
mPackageName = packageName;
return this;
}
@Override
protected void checkInitialized() {
if (mAppEntry == null || mApplicationsState == null || mPackageName == null) {
throw new IllegalStateException(
"AppEntry, AppState, and PackageName should be set before calling this "
+ "function");
}
}
@Override
protected void onCreateInternal() {
ConfirmationDialogFragment.resetListeners(
(ConfirmationDialogFragment) getFragmentController().findDialogByTag(
DISABLE_CONFIRM_DIALOG_TAG),
mDisableConfirmListener,
/* rejectListener= */ null,
/* neutralListener= */ null);
ConfirmationDialogFragment.resetListeners(
(ConfirmationDialogFragment) getFragmentController().findDialogByTag(
FORCE_STOP_CONFIRM_DIALOG_TAG),
mForceStopConfirmListener,
/* rejectListener= */ null,
/* neutralListener= */ null);
getPreference().getButton(ActionButtons.BUTTON2)
.setText(R.string.force_stop)
.setIcon(R.drawable.ic_warning)
.setOnClickListener(mForceStopClickListener)
.setEnabled(false);
mSession = mApplicationsState.newSession(mApplicationStateCallbacks);
}
@Override
protected void onStartInternal() {
mSession.onResume();
}
@Override
protected void onStopInternal() {
mSession.onPause();
}
@Override
protected void updateState(ActionButtonsPreference preference) {
refreshAppEntry();
if (mAppEntry == null) {
getFragmentController().goBack();
return;
}
updateForceStopButton();
updateUninstallButton();
}
private void refreshAppEntry() {
mAppEntry = mApplicationsState.getEntry(mPackageName, UserHandle.myUserId());
if (mAppEntry != null) {
try {
mPackageInfo = mPm.getPackageInfo(mPackageName,
PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_ANY_USER
| PackageManager.GET_SIGNATURES | PackageManager.GET_PERMISSIONS);
} catch (PackageManager.NameNotFoundException e) {
LOG.e("Exception when retrieving package:" + mPackageName, e);
mPackageInfo = null;
}
} else {
mPackageInfo = null;
}
}
private void updateForceStopButton() {
if (mDpm.packageHasActiveAdmins(mPackageName)) {
updateForceStopButtonInner(/* enabled= */ false);
} else if ((mAppEntry.info.flags & ApplicationInfo.FLAG_STOPPED) == 0) {
// If the app isn't explicitly stopped, then always show the force stop button.
updateForceStopButtonInner(/* enabled= */ true);
} else {
Intent intent = new Intent(Intent.ACTION_QUERY_PACKAGE_RESTART,
Uri.fromParts("package", mPackageName, /* fragment= */ null));
intent.putExtra(Intent.EXTRA_PACKAGES, new String[]{mPackageName});
intent.putExtra(Intent.EXTRA_UID, mAppEntry.info.uid);
intent.putExtra(Intent.EXTRA_USER_HANDLE,
UserHandle.getUserId(mAppEntry.info.uid));
LOG.d("Sending broadcast to query restart status for " + mPackageName);
getContext().sendOrderedBroadcastAsUser(intent,
UserHandle.CURRENT,
android.Manifest.permission.HANDLE_QUERY_PACKAGE_RESTART,
mCheckKillProcessesReceiver,
/* scheduler= */ null,
Activity.RESULT_CANCELED,
/* initialData= */ null,
/* initialExtras= */ null);
}
}
private void updateForceStopButtonInner(boolean enabled) {
if (enabled) {
Boolean shouldDisable = shouldDisableButtonBecauseOfUserRestriction("Force Stop",
UserManager.DISALLOW_APPS_CONTROL);
if (shouldDisable != null) {
if (shouldDisable) {
enabled = false;
} else {
mRestriction = UserManager.DISALLOW_APPS_CONTROL;
}
}
}
getPreference().getButton(ActionButtons.BUTTON2).setEnabled(enabled);
}
private void updateUninstallButtonInner(boolean isAppEnabled) {
ActionButtonInfo uninstallButton = getPreference().getButton(ActionButtons.BUTTON1);
if (isBundledApp()) {
if (isAppEnabled) {
uninstallButton.setText(R.string.disable_text).setIcon(
R.drawable.ic_block).setOnClickListener(mDisableClickListener);
} else {
uninstallButton.setText(R.string.enable_text).setIcon(
R.drawable.ic_check_circle).setOnClickListener(mEnableClickListener);
}
} else {
uninstallButton.setText(R.string.uninstall_text).setIcon(
R.drawable.ic_delete).setOnClickListener(mUninstallClickListener);
}
uninstallButton.setEnabled(!shouldDisableUninstallButton());
}
private void updateUninstallButton() {
updateUninstallButtonInner(isAppEnabled());
}
private boolean shouldDisableUninstallButton() {
if (shouldDisableUninstallForHomeApp()) {
LOG.d("Uninstall disabled for home app");
return true;
}
if (isAppEnabled() && isKeepEnabledPackage(getContext(), mPackageName)) {
LOG.d("Disable button disabled for keep enabled package");
return true;
}
if (Utils.isSystemPackage(getContext().getResources(), mPm, mPackageInfo)) {
LOG.d("Uninstall disabled for system package");
return true;
}
// We don't allow uninstalling profile/device owner on any profile because if it's a system
// app, "uninstall" is actually "downgrade to the system version + disable", and
// "downgrade" will clear data on all profiles.
if (isProfileOrDeviceOwner(mPackageName, mDpm, mProfileHelper)) {
LOG.d("Uninstall disabled because package is profile or device owner");
return true;
}
if (mDpm.isUninstallInQueue(mPackageName)) {
LOG.d("Uninstall disabled because intent is already queued");
return true;
}
Boolean shouldDisable = shouldDisableButtonBecauseOfUserRestriction("Uninstall",
UserManager.DISALLOW_APPS_CONTROL);
if (shouldDisable != null) return shouldDisable;
shouldDisable = shouldDisableButtonBecauseOfUserRestriction("Uninstall",
UserManager.DISALLOW_UNINSTALL_APPS);
if (shouldDisable != null) return shouldDisable;
return false;
}
/**
* Checks whether a button should be disabled because the user has the given restriction
* (and whether the restriction was was set by a device admin).
*
* @param button action name (for logging purposes)
* @param restriction user restriction
*
* @return {@code null} if the user doesn't have the restriction, {@value Boolean#TRUE} if it
* should be disabled because of {@link UserManager} restrictions, or {@value Boolean#FALSE} if
* should not be disabled because of {@link DevicePolicyManager} restrictions (in which case
* {@link #mRestriction} is updated with the restriction).
*/
@Nullable
private Boolean shouldDisableButtonBecauseOfUserRestriction(String button, String restriction) {
if (!mUserManager.hasUserRestriction(restriction)) return null;
UserHandle user = UserHandle.getUserHandleForUid(mAppEntry.info.uid);
if (mUserManager.hasBaseUserRestriction(restriction, user)) {
LOG.d(button + " disabled because " + user + " has " + restriction + " restriction");
return Boolean.TRUE;
}
LOG.d(button + " NOT disabled because " + user + " has " + restriction + " restriction but "
+ "it was set by a device admin (it will show a dialog explaining that instead)");
mRestriction = restriction;
return Boolean.FALSE;
}
/**
* Returns {@code true} if the package is a Home app that should not be uninstalled. We don't
* risk downgrading bundled home apps because that can interfere with home-key resolution. We
* can't allow removal of the only home app, and we don't want to allow removal of an
* explicitly preferred home app. The user can go to Home settings and pick a different app,
* after which we'll permit removal of the now-not-default app.
*/
private boolean shouldDisableUninstallForHomeApp() {
Set<String> homePackages = new ArraySet<>();
// Get list of "home" apps and trace through any meta-data references.
List<ResolveInfo> homeActivities = new ArrayList<>();
ComponentName currentDefaultHome = mPm.getHomeActivities(homeActivities);
for (int i = 0; i < homeActivities.size(); i++) {
ResolveInfo ri = homeActivities.get(i);
String activityPkg = ri.activityInfo.packageName;
homePackages.add(activityPkg);
// Also make sure to include anything proxying for the home app.
Bundle metadata = ri.activityInfo.metaData;
if (metadata != null) {
String metaPkg = metadata.getString(ActivityManager.META_HOME_ALTERNATE);
if (signaturesMatch(metaPkg, activityPkg)) {
homePackages.add(metaPkg);
}
}
}
if (homePackages.contains(mPackageName)) {
if (isBundledApp()) {
// Don't risk a downgrade.
return true;
} else if (currentDefaultHome == null) {
// No preferred default. Permit uninstall only when there is more than one
// candidate.
return (homePackages.size() == 1);
} else {
// Explicit default home app. Forbid uninstall of that one, but permit it for
// installed-but-inactive ones.
return mPackageName.equals(currentDefaultHome.getPackageName());
}
} else {
// Not a home app.
return false;
}
}
private boolean signaturesMatch(String pkg1, String pkg2) {
if (pkg1 != null && pkg2 != null) {
try {
int match = mPm.checkSignatures(pkg1, pkg2);
if (match >= PackageManager.SIGNATURE_MATCH) {
return true;
}
} catch (Exception e) {
// e.g. package not found during lookup. Possibly bad input.
// Just return false as this isn't a reason to crash given the use case.
}
}
return false;
}
private boolean isBundledApp() {
return (mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
}
private boolean isAppEnabled() {
return mAppEntry.info.enabled && !(mAppEntry.info.enabledSetting
== PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED);
}
@Override
public void processActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == UNINSTALL_REQUEST_CODE
|| requestCode == UNINSTALL_DEVICE_ADMIN_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
getFragmentController().goBack();
} else if (resultCode == RESULT_FIRST_USER) {
showUninstallBlockedByAdminDialog();
LOG.e("Uninstall failed");
}
}
}
private void showUninstallBlockedByAdminDialog() {
getFragmentController().showDialog(
EnterpriseUtils.getActionDisabledByAdminDialog(getContext(),
BLOCKED_UNINSTALL_APP, mPackageName),
DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG);
}
private boolean ignoreActionBecauseItsDisabledByAdmin(List<String> restrictions) {
if (mRestriction == null || !restrictions.contains(mRestriction)) return false;
LOG.d("Ignoring action because of " + mRestriction);
getFragmentController().showDialog(ActionDisabledByAdminDialogFragment.newInstance(
mRestriction, UserHandle.myUserId()), DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG);
return true;
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.UserHandle;
import androidx.annotation.XmlRes;
import com.android.car.settings.R;
import com.android.car.settings.applications.appinfo.AppAllServicesPreferenceController;
import com.android.car.settings.applications.appinfo.HibernationSwitchPreferenceController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.SettingsFragment;
import com.android.settingslib.applications.ApplicationsState;
/**
* Shows details about an application.
*/
public class ApplicationDetailsFragment extends SettingsFragment {
private static final Logger LOG = new Logger(ApplicationDetailsFragment.class);
public static final String EXTRA_PACKAGE_NAME = "extra_package_name";
private PackageManager mPm;
private String mPackageName;
private PackageInfo mPackageInfo;
private ApplicationsState mAppState;
private ApplicationsState.AppEntry mAppEntry;
/** Creates an instance of this fragment, passing packageName as an argument. */
public static ApplicationDetailsFragment getInstance(String packageName) {
ApplicationDetailsFragment applicationDetailFragment = new ApplicationDetailsFragment();
Bundle bundle = new Bundle();
bundle.putString(EXTRA_PACKAGE_NAME, packageName);
applicationDetailFragment.setArguments(bundle);
return applicationDetailFragment;
}
@Override
@XmlRes
protected int getPreferenceScreenResId() {
return R.xml.application_details_fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mPm = context.getPackageManager();
// These should be loaded before onCreate() so that the controller operates as expected.
mPackageName = getArguments().getString(EXTRA_PACKAGE_NAME);
mAppState = ApplicationsState.getInstance(requireActivity().getApplication());
retrieveAppEntry();
use(ApplicationPreferenceController.class,
R.string.pk_application_details_app)
.setAppEntry(mAppEntry).setAppState(mAppState);
use(ApplicationActionButtonsPreferenceController.class,
R.string.pk_application_details_action_buttons)
.setAppEntry(mAppEntry).setAppState(mAppState).setPackageName(mPackageName);
use(AppAllServicesPreferenceController.class,
R.string.pk_all_services_settings).setPackageName(mPackageName);
use(NotificationsPreferenceController.class,
R.string.pk_application_details_notifications).setPackageInfo(mPackageInfo);
use(PermissionsPreferenceController.class,
R.string.pk_application_details_permissions).setPackageName(mPackageName);
use(StoragePreferenceController.class,
R.string.pk_application_details_storage)
.setAppEntry(mAppEntry).setAppState(mAppState).setPackageName(mPackageName);
use(PrioritizeAppPerformancePreferenceController.class,
R.string.pk_application_details_prioritize_app_performance)
.setPackageInfo(mPackageInfo);
use(HibernationSwitchPreferenceController.class,
R.string.pk_hibernation_switch)
.setPackageName(mPackageName);
use(VersionPreferenceController.class,
R.string.pk_application_details_version).setPackageInfo(mPackageInfo);
}
private void retrieveAppEntry() {
mAppEntry = mAppState.getEntry(mPackageName, UserHandle.myUserId());
if (mAppEntry != null) {
try {
mPackageInfo = mPm.getPackageInfo(mPackageName,
PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_ANY_USER
| PackageManager.GET_SIGNATURES | PackageManager.GET_PERMISSIONS);
} catch (PackageManager.NameNotFoundException e) {
LOG.e("Exception when retrieving package:" + mPackageName, e);
mPackageInfo = null;
}
} else {
mPackageInfo = null;
}
}
}

View File

@@ -0,0 +1,266 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.os.Handler;
import android.os.storage.VolumeInfo;
import androidx.lifecycle.Lifecycle;
import com.android.car.settings.common.Logger;
import com.android.settingslib.applications.ApplicationsState;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Class used to load the applications installed on the system with their metadata.
*/
// TODO: consolidate with AppEntryListManager.
public class ApplicationListItemManager implements ApplicationsState.Callbacks {
/**
* Callback that is called once the list of applications are loaded.
*/
public interface AppListItemListener {
/**
* Called when the data is successfully loaded from {@link ApplicationsState.Callbacks} and
* icon, title and summary are set for all the applications.
*/
void onDataLoaded(ArrayList<ApplicationsState.AppEntry> apps);
}
private static final Logger LOG = new Logger(ApplicationListItemManager.class);
private static final String APP_NAME_UNKNOWN = "APP NAME UNKNOWN";
private final VolumeInfo mVolumeInfo;
private final Lifecycle mLifecycle;
private final ApplicationsState mAppState;
private final List<AppListItemListener> mAppListItemListeners = new ArrayList<>();
private final Handler mHandler;
private final int mMillisecondUpdateInterval;
// Milliseconds that warnIfNotAllLoadedInTime method waits before comparing mAppsToLoad and
// mLoadedApps to log any apps that failed to load.
private final int mMaxAppLoadWaitInterval;
private ApplicationsState.Session mSession;
private ApplicationsState.AppFilter mAppFilter;
private Comparator<ApplicationsState.AppEntry> mAppEntryComparator;
// Contains all of the apps that we are expecting to load.
private Set<ApplicationsState.AppEntry> mAppsToLoad = new HashSet<>();
// Contains all apps that have been successfully loaded.
private ArrayList<ApplicationsState.AppEntry> mLoadedApps = new ArrayList<>();
// Indicates whether onRebuildComplete's throttling is off and it is ready to render updates.
// onRebuildComplete uses throttling to prevent it from being called too often, since the
// animation can be choppy if the refresh rate is too high.
private boolean mReadyToRenderUpdates = true;
// Parameter we use to call onRebuildComplete method when the throttling is off and we are
// "ReadyToRenderUpdates" again.
private ArrayList<ApplicationsState.AppEntry> mDeferredAppsToUpload;
public ApplicationListItemManager(VolumeInfo volumeInfo, Lifecycle lifecycle,
ApplicationsState appState, int millisecondUpdateInterval,
int maxWaitIntervalToFinishLoading) {
mVolumeInfo = volumeInfo;
mLifecycle = lifecycle;
mAppState = appState;
mHandler = new Handler();
mMillisecondUpdateInterval = millisecondUpdateInterval;
mMaxAppLoadWaitInterval = maxWaitIntervalToFinishLoading;
}
/**
* Registers a listener that will be notified once the data is loaded.
*/
public void registerListener(AppListItemListener appListItemListener) {
if (!mAppListItemListeners.contains(appListItemListener) && appListItemListener != null) {
mAppListItemListeners.add(appListItemListener);
}
}
/**
* Unregisters the listener.
*/
public void unregisterlistener(AppListItemListener appListItemListener) {
mAppListItemListeners.remove(appListItemListener);
}
/**
* Resumes the session and starts meauring app loading time on fragment start.
*/
public void onFragmentStart() {
mSession.onResume();
warnIfNotAllLoadedInTime();
}
/**
* Pause the session on fragment stop.
*/
public void onFragmentStop() {
mSession.onPause();
}
/**
* Starts the new session and start loading the list of installed applications on the device.
* This list will be filtered out based on the {@link ApplicationsState.AppFilter} provided.
* Once the list is ready, {@link AppListItemListener#onDataLoaded} will be called.
*
* @param appFilter based on which the list of applications will be filtered before
* returning.
* @param appEntryComparator comparator based on which the application list will be sorted.
*/
public void startLoading(ApplicationsState.AppFilter appFilter,
Comparator<ApplicationsState.AppEntry> appEntryComparator) {
if (mSession != null) {
LOG.w("Loading already started but restart attempted.");
return; // Prevent leaking sessions.
}
mAppFilter = appFilter;
mAppEntryComparator = appEntryComparator;
mSession = mAppState.newSession(this, mLifecycle);
}
/**
* Rebuilds the list of applications using the provided {@link ApplicationsState.AppFilter}.
* The filter will be used for all subsequent loading. Once the list is ready, {@link
* AppListItemListener#onDataLoaded} will be called.
*/
public void rebuildWithFilter(ApplicationsState.AppFilter appFilter) {
mAppFilter = appFilter;
rebuild();
}
@Override
public void onPackageIconChanged() {
rebuild();
}
@Override
public void onPackageSizeChanged(String packageName) {
rebuild();
}
@Override
public void onAllSizesComputed() {
rebuild();
}
@Override
public void onLauncherInfoChanged() {
rebuild();
}
@Override
public void onLoadEntriesCompleted() {
rebuild();
}
@Override
public void onRunningStateChanged(boolean running) {
}
@Override
public void onPackageListChanged() {
rebuild();
}
@Override
public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
// Checking for apps.size prevents us from unnecessarily triggering throttling and blocking
// subsequent updates.
if (apps.size() == 0) {
return;
}
if (mReadyToRenderUpdates) {
mReadyToRenderUpdates = false;
mLoadedApps = new ArrayList<>();
for (ApplicationsState.AppEntry app : apps) {
if (isLoaded(app)) {
mLoadedApps.add(app);
}
}
for (AppListItemListener appListItemListener : mAppListItemListeners) {
appListItemListener.onDataLoaded(mLoadedApps);
}
mHandler.postDelayed(() -> {
mReadyToRenderUpdates = true;
if (mDeferredAppsToUpload != null) {
onRebuildComplete(mDeferredAppsToUpload);
mDeferredAppsToUpload = null;
}
}, mMillisecondUpdateInterval);
} else {
mDeferredAppsToUpload = apps;
}
// Add all apps that are not already contained in mAppsToLoad Set, since we want it to be an
// exhaustive Set of all apps to be loaded.
mAppsToLoad.addAll(apps);
}
private boolean isLoaded(ApplicationsState.AppEntry app) {
return app.label != null && app.sizeStr != null && app.icon != null;
}
private void warnIfNotAllLoadedInTime() {
mHandler.postDelayed(() -> {
if (mLoadedApps.size() < mAppsToLoad.size()) {
LOG.w("Expected to load " + mAppsToLoad.size() + " apps but only loaded "
+ mLoadedApps.size());
// Creating a copy to avoid state inconsistency.
Set<ApplicationsState.AppEntry> appsToLoadCopy = new HashSet(mAppsToLoad);
for (ApplicationsState.AppEntry loadedApp : mLoadedApps) {
appsToLoadCopy.remove(loadedApp);
}
for (ApplicationsState.AppEntry appEntry : appsToLoadCopy) {
String appName = appEntry.label == null ? APP_NAME_UNKNOWN : appEntry.label;
LOG.w("App failed to load: " + appName);
}
}
}, mMaxAppLoadWaitInterval);
}
ApplicationsState.AppFilter getCompositeFilter(String volumeUuid) {
if (mAppFilter == null) {
return null;
}
ApplicationsState.AppFilter filter = new ApplicationsState.VolumeFilter(volumeUuid);
filter = new ApplicationsState.CompoundFilter(mAppFilter, filter);
return filter;
}
private void rebuild() {
ApplicationsState.AppFilter filterObj = ApplicationsState.FILTER_EVERYTHING;
filterObj = new ApplicationsState.CompoundFilter(filterObj,
ApplicationsState.FILTER_NOT_HIDE);
ApplicationsState.AppFilter compositeFilter = getCompositeFilter(mVolumeInfo.getFsUuid());
if (compositeFilter != null) {
filterObj = new ApplicationsState.CompoundFilter(filterObj, compositeFilter);
}
ApplicationsState.AppFilter finalFilterObj = filterObj;
mSession.rebuild(finalFilterObj, mAppEntryComparator, /* foreground= */ false);
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.graphics.drawable.Drawable;
import androidx.preference.Preference;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
/** Business logic for the Application field in the application details page. */
public class ApplicationPreferenceController extends PreferenceController<Preference> {
private AppEntry mAppEntry;
private ApplicationsState mApplicationsState;
public ApplicationPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
/** Sets the {@link AppEntry} which is used to load the app name and icon. */
public ApplicationPreferenceController setAppEntry(AppEntry appEntry) {
mAppEntry = appEntry;
return this;
}
/** Sets the {@link ApplicationsState} which is used to load the app name and icon. */
public ApplicationPreferenceController setAppState(ApplicationsState applicationsState) {
mApplicationsState = applicationsState;
return this;
}
@Override
protected void checkInitialized() {
if (mAppEntry == null || mApplicationsState == null) {
throw new IllegalStateException(
"AppEntry and AppState should be set before calling this function");
}
}
@Override
protected void updateState(Preference preference) {
preference.setTitle(getAppName());
preference.setIcon(getAppIcon());
}
protected String getAppName() {
mAppEntry.ensureLabel(getContext());
return mAppEntry.label;
}
protected Drawable getAppIcon() {
mApplicationsState.ensureIcon(mAppEntry);
return mAppEntry.icon;
}
protected String getAppVersion() {
return mAppEntry.getVersion(getContext());
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import static com.android.car.settings.storage.StorageUtils.maybeInitializeVolume;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import com.android.car.settings.R;
import com.android.settingslib.applications.ApplicationsState;
/**
* Lists all installed applications and their summary.
*/
public class ApplicationsSettingsFragment extends AppListFragment {
private ApplicationListItemManager mAppListItemManager;
@Override
protected int getPreferenceScreenResId() {
return R.xml.applications_settings_fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
Application application = requireActivity().getApplication();
StorageManager sm = context.getSystemService(StorageManager.class);
VolumeInfo volume = maybeInitializeVolume(sm, getArguments());
mAppListItemManager = new ApplicationListItemManager(volume, getLifecycle(),
ApplicationsState.getInstance(application),
getContext().getResources().getInteger(
R.integer.millisecond_app_data_update_interval),
getContext().getResources().getInteger(
R.integer.millisecond_max_app_load_wait_interval));
mAppListItemManager.registerListener(
use(ApplicationsSettingsPreferenceController.class,
R.string.pk_all_applications_settings_list));
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAppListItemManager.startLoading(getAppFilter(), ApplicationsState.ALPHA_COMPARATOR);
}
@Override
public void onStart() {
super.onStart();
mAppListItemManager.onFragmentStart();
}
@Override
public void onStop() {
super.onStop();
mAppListItemManager.onFragmentStop();
}
@Override
protected void onToggleShowSystemApps(boolean showSystem) {
mAppListItemManager.rebuildWithFilter(getAppFilter());
}
private ApplicationsState.AppFilter getAppFilter() {
return shouldShowSystemApps() ? null
: ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER_AND_INSTANT;
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.graphics.drawable.Drawable;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.car.ui.preference.CarUiPreference;
import com.android.settingslib.applications.ApplicationsState;
import java.util.ArrayList;
/** Business logic which populates the applications in this setting. */
public class ApplicationsSettingsPreferenceController extends
PreferenceController<PreferenceGroup> implements
ApplicationListItemManager.AppListItemListener {
public ApplicationsSettingsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<PreferenceGroup> getPreferenceType() {
return PreferenceGroup.class;
}
@Override
public void onDataLoaded(ArrayList<ApplicationsState.AppEntry> apps) {
getPreference().removeAll();
for (ApplicationsState.AppEntry appEntry : apps) {
getPreference().addPreference(
createPreference(appEntry.label, appEntry.sizeStr, appEntry.icon,
appEntry.info.packageName));
}
}
private Preference createPreference(String title, String summary, Drawable icon,
String packageName) {
CarUiPreference preference = new CarUiPreference(getContext());
preference.setTitle(title);
preference.setSummary(summary);
preference.setIcon(icon);
preference.setKey(packageName);
preference.setOnPreferenceClickListener(p -> {
getFragmentController().launchFragment(
ApplicationDetailsFragment.getInstance(packageName));
return true;
});
return preference;
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.UserInfo;
import android.provider.DeviceConfig;
import android.telecom.DefaultDialerManager;
import android.text.TextUtils;
import android.util.ArraySet;
import com.android.car.settings.profiles.ProfileHelper;
import com.android.internal.telephony.SmsApplication;
import java.util.List;
import java.util.Set;
/** Utility functions for use in applications settings. */
public class ApplicationsUtils {
/** Whether or not app hibernation is enabled on the device **/
public static final String PROPERTY_APP_HIBERNATION_ENABLED = "app_hibernation_enabled";
private ApplicationsUtils() {
}
/**
* Returns {@code true} if the package should always remain enabled.
*/
// TODO: investigate making this behavior configurable via a feature factory with the current
// contents as the default.
public static boolean isKeepEnabledPackage(Context context, String packageName) {
// Find current default phone/sms app. We should keep them enabled.
Set<String> keepEnabledPackages = new ArraySet<>();
String defaultDialer = DefaultDialerManager.getDefaultDialerApplication(context);
if (!TextUtils.isEmpty(defaultDialer)) {
keepEnabledPackages.add(defaultDialer);
}
ComponentName defaultSms = SmsApplication.getDefaultSmsApplication(
context, /* updateIfNeeded= */ true);
if (defaultSms != null) {
keepEnabledPackages.add(defaultSms.getPackageName());
}
return keepEnabledPackages.contains(packageName);
}
/**
* Returns {@code true} if the given {@code packageName} is device owner or profile owner of at
* least one user.
*/
public static boolean isProfileOrDeviceOwner(String packageName, DevicePolicyManager dpm,
ProfileHelper profileHelper) {
if (dpm.isDeviceOwnerAppOnAnyUser(packageName)) {
return true;
}
List<UserInfo> userInfos = profileHelper.getAllProfiles();
for (int i = 0; i < userInfos.size(); i++) {
ComponentName cn = dpm.getProfileOwnerAsUser(userInfos.get(i).id);
if (cn != null && cn.getPackageName().equals(packageName)) {
return true;
}
}
return false;
}
/**
* Returns {@code true} if the hibernation feature is enabled, as configured through {@link
* DeviceConfig}, which can be overridden remotely with a flag or through adb.
*/
public static boolean isHibernationEnabled() {
return DeviceConfig.getBoolean(
NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, true);
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.content.Context;
import android.os.Bundle;
import android.provider.Settings;
import androidx.annotation.XmlRes;
import com.android.car.settings.R;
import com.android.car.settings.applications.performance.PerfImpactingAppsItemManager;
import com.android.car.settings.common.SettingsFragment;
import com.android.car.settings.search.CarBaseSearchIndexProvider;
import com.android.settingslib.search.SearchIndexable;
/** Shows subsettings related to apps. */
@SearchIndexable
public class AppsFragment extends SettingsFragment {
private RecentAppsItemManager mRecentAppsItemManager;
private InstalledAppCountItemManager mInstalledAppCountItemManager;
private HibernatedAppsItemManager mHibernatedAppsItemManager;
private PerfImpactingAppsItemManager mPerfImpactingAppsItemManager;
@Override
@XmlRes
protected int getPreferenceScreenResId() {
return R.xml.apps_fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mRecentAppsItemManager = new RecentAppsItemManager(context,
context.getResources().getInteger(R.integer.recent_apps_max_count));
mRecentAppsItemManager.addListener(use(AllAppsPreferenceController.class,
R.string.pk_applications_settings_screen_entry));
mRecentAppsItemManager.addListener(use(RecentAppsGroupPreferenceController.class,
R.string.pk_recent_apps_group));
mRecentAppsItemManager.addListener(use(RecentAppsListPreferenceController.class,
R.string.pk_recent_apps_list));
mInstalledAppCountItemManager = new InstalledAppCountItemManager(context);
mInstalledAppCountItemManager.addListener(use(AllAppsPreferenceController.class,
R.string.pk_applications_settings_screen_entry));
mInstalledAppCountItemManager.addListener(use(RecentAppsViewAllPreferenceController.class,
R.string.pk_recent_apps_view_all));
mHibernatedAppsItemManager = new HibernatedAppsItemManager(context);
mHibernatedAppsItemManager.setListener(use(HibernatedAppsPreferenceController.class,
R.string.pk_hibernated_apps));
mPerfImpactingAppsItemManager = new PerfImpactingAppsItemManager(context);
mPerfImpactingAppsItemManager.addListener(
use(PerfImpactingAppsEntryPreferenceController.class,
R.string.pk_performance_impacting_apps_entry));
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRecentAppsItemManager.startLoading();
mInstalledAppCountItemManager.startLoading();
mHibernatedAppsItemManager.startLoading();
}
@Override
public void onResume() {
super.onResume();
mPerfImpactingAppsItemManager.startLoading();
}
/**
* Data provider for Settings Search.
*/
public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new CarBaseSearchIndexProvider(R.xml.apps_fragment,
Settings.ACTION_APPLICATION_SETTINGS);
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.content.Context;
import android.permission.PermissionControllerManager;
import androidx.annotation.NonNull;
/**
* Class for fetching and returning the number of hibernated apps. Largely derived from
* {@link com.android.settings.applications.HibernatedAppsPreferenceController}.
*/
public class HibernatedAppsItemManager {
private final Context mContext;
private HibernatedAppsCountListener mListener;
public HibernatedAppsItemManager(Context context) {
mContext = context;
}
/**
* Starts fetching recently used apps
*/
public void startLoading() {
PermissionControllerManager permController =
mContext.getSystemService(PermissionControllerManager.class);
if (mListener != null && permController != null) {
// This executor is only used for returning the value
// The main logic happens on a background thread
permController.getUnusedAppCount(mContext.getMainExecutor(),
mListener::onHibernatedAppsCountLoaded);
}
}
/**
* Registers a listener that will be notified once the data is loaded.
*/
public void setListener(@NonNull HibernatedAppsCountListener listener) {
mListener = listener;
}
/**
* Callback that is called once the count of hibernated apps has been fetched.
*/
public interface HibernatedAppsCountListener {
/**
* Called when the count of hibernated apps has loaded.
*/
void onHibernatedAppsCountLoaded(int hibernatedAppsCount);
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import static com.android.car.settings.applications.ApplicationsUtils.isHibernationEnabled;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.settingslib.utils.StringUtil;
/**
* A preference controller handling the logic for updating summary of hibernated apps.
*/
public final class HibernatedAppsPreferenceController extends PreferenceController<Preference>
implements HibernatedAppsItemManager.HibernatedAppsCountListener {
private static final String TAG = "HibernatedAppsPrefController";
public HibernatedAppsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
@Override
public int getDefaultAvailabilityStatus() {
return isHibernationEnabled() ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
}
@Override
public void onHibernatedAppsCountLoaded(int hibernatedAppsCount) {
getPreference().setSummary(StringUtil.getIcuPluralsString(getContext(), hibernatedAppsCount,
R.string.unused_apps_summary));
refreshUi();
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.SharedPreferences;
import com.android.car.settings.common.ColoredSwitchPreference;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* Hides/shows system apps from Application listings. Intended to be used inside instances of
* {@link AppListFragment}.
*/
public class HideSystemSwitchPreferenceController
extends PreferenceController<ColoredSwitchPreference> {
private final SharedPreferences mSharedPreferences = getContext().getSharedPreferences(
AppListFragment.SHARED_PREFERENCE_PATH, Context.MODE_PRIVATE);
public HideSystemSwitchPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<ColoredSwitchPreference> getPreferenceType() {
return ColoredSwitchPreference.class;
}
@Override
protected boolean handlePreferenceChanged(ColoredSwitchPreference preference, Object newValue) {
boolean checked = (Boolean) newValue;
mSharedPreferences.edit().putBoolean(AppListFragment.KEY_HIDE_SYSTEM, checked).apply();
return true;
}
@Override
protected void onStartInternal() {
getPreference().setChecked(getSharedPreferenceHidden());
}
private boolean getSharedPreferenceHidden() {
return mSharedPreferences.getBoolean(AppListFragment.KEY_HIDE_SYSTEM,
/* defaultValue= */ true);
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
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.UserHandle;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Class used to count the number of non-system apps. Largely derived from
* {@link com.android.settings.applications.InstalledAppCounter}.
*/
public class InstalledAppCountItemManager {
private Context mContext;
private final List<InstalledAppCountListener> mInstalledAppCountListeners;
public InstalledAppCountItemManager(Context context) {
mContext = context;
mInstalledAppCountListeners = new ArrayList<>();
}
/**
* Registers a listener that will be notified once the data is loaded.
*/
public void addListener(@NonNull InstalledAppCountListener listener) {
mInstalledAppCountListeners.add(listener);
}
/**
* Starts fetching installed apps and counting the non-system apps
*/
public void startLoading() {
ThreadUtils.postOnBackgroundThread(() -> {
List<ApplicationInfo> appList = mContext.getPackageManager()
.getInstalledApplications(PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS);
int appCount = 0;
for (ApplicationInfo applicationInfo : appList) {
if (shouldCountApp(applicationInfo)) {
appCount++;
}
}
int finalAppCount = appCount;
for (InstalledAppCountListener listener : mInstalledAppCountListeners) {
ThreadUtils.postOnMainThread(() -> listener
.onInstalledAppCountLoaded(finalAppCount));
}
});
}
@VisibleForTesting
boolean shouldCountApp(ApplicationInfo applicationInfo) {
if ((applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
return true;
}
if ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
return true;
}
int userId = UserHandle.getUserId(applicationInfo.uid);
Intent launchIntent = new Intent(Intent.ACTION_MAIN, null)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setPackage(applicationInfo.packageName);
List<ResolveInfo> intents = mContext.getPackageManager().queryIntentActivitiesAsUser(
launchIntent,
PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
userId);
return intents != null && !intents.isEmpty();
}
/**
* Callback that is called once the number of installed applications is counted.
*/
public interface InstalledAppCountListener {
/**
* Called when the apps are successfully loaded from PackageManager and non-system apps are
* counted.
*/
void onInstalledAppCountLoaded(int appCount);
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import androidx.preference.TwoStatePreference;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.notifications.BaseNotificationsPreferenceController;
/**
* Controller for preference which enables / disables showing notifications for an application.
*/
public class NotificationsPreferenceController extends
BaseNotificationsPreferenceController<TwoStatePreference> {
private static final Logger LOG = new Logger(NotificationsPreferenceController.class);
private String mPackageName;
private int mUid;
private ApplicationInfo mAppInfo;
public NotificationsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
/**
* Set the package info of the application.
*/
public void setPackageInfo(PackageInfo packageInfo) {
mPackageName = packageInfo.packageName;
mUid = packageInfo.applicationInfo.uid;
mAppInfo = packageInfo.applicationInfo;
}
@Override
protected Class<TwoStatePreference> getPreferenceType() {
return TwoStatePreference.class;
}
@Override
protected void updateState(TwoStatePreference preference) {
preference.setChecked(areNotificationsEnabled(mPackageName, mUid));
preference.setEnabled(areNotificationsChangeable(mAppInfo));
}
@Override
protected boolean handlePreferenceChanged(TwoStatePreference preference, Object newValue) {
boolean enabled = (boolean) newValue;
return toggleNotificationsSetting(mPackageName, mUid, enabled);
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.applications.performance.PerfImpactingAppsItemManager;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.settingslib.utils.StringUtil;
/**
* Controller for the entry point to performance-impacting apps settings. It fetches the number of
* resource overuse packages and updates the entry point preference's summary.
*/
public final class PerfImpactingAppsEntryPreferenceController extends
PreferenceController<Preference> implements
PerfImpactingAppsItemManager.PerfImpactingAppsListener {
public PerfImpactingAppsEntryPreferenceController(Context context,
String preferenceKey,
FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
@Override
public void onPerfImpactingAppsLoaded(int disabledPackagesCount) {
getPreference().setSummary(StringUtil.getIcuPluralsString(getContext(),
disabledPackagesCount, R.string.performance_impacting_apps_summary));
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.icu.text.ListFormatter;
import android.text.TextUtils;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.PreferenceController;
import com.android.settingslib.applications.PermissionsSummaryHelper;
import com.android.settingslib.utils.StringUtil;
import java.util.ArrayList;
import java.util.List;
/** Business logic for the permissions entry in the application details settings. */
public class PermissionsPreferenceController extends PreferenceController<Preference> {
private static final Logger LOG = new Logger(PermissionsPreferenceController.class);
private String mPackageName;
private String mSummary;
public PermissionsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
/**
* Set the packageName, which is used on the intent to open the permissions
* selection screen.
*/
public void setPackageName(String packageName) {
mPackageName = packageName;
}
@Override
protected void checkInitialized() {
if (mPackageName == null) {
throw new IllegalStateException(
"PackageName should be set before calling this function");
}
}
@Override
protected void onStartInternal() {
PermissionsSummaryHelper.getPermissionSummary(getContext(), mPackageName,
mPermissionCallback);
}
@Override
protected void updateState(Preference preference) {
preference.setSummary(getSummary());
}
@Override
protected boolean handlePreferenceClicked(Preference preference) {
Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mPackageName);
try {
getContext().startActivity(intent);
} catch (ActivityNotFoundException e) {
LOG.w("No app can handle android.intent.action.MANAGE_APP_PERMISSIONS");
}
return true;
}
private CharSequence getSummary() {
if (TextUtils.isEmpty(mSummary)) {
return getContext().getString(R.string.computing_size);
}
return mSummary;
}
private final PermissionsSummaryHelper.PermissionsResultCallback mPermissionCallback =
new PermissionsSummaryHelper.PermissionsResultCallback() {
@Override
public void onPermissionSummaryResult(
int requestedPermissionCount, int additionalGrantedPermissionCount,
List<CharSequence> grantedGroupLabels) {
Resources res = getContext().getResources();
if (requestedPermissionCount == 0) {
mSummary = res.getString(
R.string.runtime_permissions_summary_no_permissions_requested);
} else {
ArrayList<CharSequence> list = new ArrayList<>(grantedGroupLabels);
if (additionalGrantedPermissionCount > 0) {
// N additional permissions.
list.add(StringUtil.getIcuPluralsString(getContext(),
additionalGrantedPermissionCount,
R.string.runtime_permissions_additional_count));
}
if (list.isEmpty()) {
mSummary = res.getString(
R.string.runtime_permissions_summary_no_permissions_granted);
} else {
mSummary = ListFormatter.getInstance().format(list);
}
}
refreshUi();
}
};
}

View File

@@ -0,0 +1,154 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.car.Car;
import android.car.drivingstate.CarUxRestrictions;
import android.car.watchdog.CarWatchdogManager;
import android.car.watchdog.PackageKillableState;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.os.UserHandle;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.TwoStatePreference;
import com.android.car.settings.applications.performance.PerfImpactingAppsUtils;
import com.android.car.settings.common.ConfirmationDialogFragment;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.PreferenceController;
/** Controller for preference which turns on / off prioritize app performance setting. */
public class PrioritizeAppPerformancePreferenceController
extends PreferenceController<TwoStatePreference> {
private static final Logger LOG =
new Logger(PrioritizeAppPerformancePreferenceController.class);
@VisibleForTesting
static final String TURN_ON_PRIORITIZE_APP_PERFORMANCE_DIALOG_TAG =
"com.android.car.settings.applications.TurnOnPrioritizeAppPerformanceDialogTag";
@Nullable
private Car mCar;
@Nullable
private CarWatchdogManager mCarWatchdogManager;
private String mPackageName;
private UserHandle mUserHandle;
private final ConfirmationDialogFragment.ConfirmListener mConfirmListener = arguments -> {
if (!isCarConnected()) {
return;
}
setKillableState(false);
getPreference().setChecked(true);
};
public PrioritizeAppPerformancePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected void onCreateInternal() {
connectToCar();
ConfirmationDialogFragment dialogFragment =
(ConfirmationDialogFragment) getFragmentController().findDialogByTag(
TURN_ON_PRIORITIZE_APP_PERFORMANCE_DIALOG_TAG);
ConfirmationDialogFragment.resetListeners(
dialogFragment, mConfirmListener, /* rejectListener= */ null,
/* neutralListener= */ null);
}
@Override
protected void onDestroyInternal() {
if (mCar != null) {
mCar.disconnect();
mCar = null;
}
}
/**
* Set the package info of the application.
*/
public void setPackageInfo(PackageInfo packageInfo) {
mPackageName = packageInfo.packageName;
mUserHandle = UserHandle.getUserHandleForUid(packageInfo.applicationInfo.uid);
}
@Override
protected Class<TwoStatePreference> getPreferenceType() {
return TwoStatePreference.class;
}
@Override
protected void updateState(TwoStatePreference preference) {
if (!isCarConnected()) {
return;
}
int killableState = PerfImpactingAppsUtils.getKillableState(mPackageName, mUserHandle,
mCarWatchdogManager);
preference.setChecked(killableState == PackageKillableState.KILLABLE_STATE_NO);
preference.setEnabled(killableState != PackageKillableState.KILLABLE_STATE_NEVER);
}
@Override
protected boolean handlePreferenceChanged(TwoStatePreference preference, Object newValue) {
boolean isToggledOn = (boolean) newValue;
if (isToggledOn) {
PerfImpactingAppsUtils.showPrioritizeAppConfirmationDialog(getContext(),
getFragmentController(), mConfirmListener,
TURN_ON_PRIORITIZE_APP_PERFORMANCE_DIALOG_TAG);
return false;
}
if (!isCarConnected()) {
return false;
}
setKillableState(true);
return true;
}
private void setKillableState(boolean isKillable) {
mCarWatchdogManager.setKillablePackageAsUser(mPackageName, mUserHandle, isKillable);
}
private boolean isCarConnected() {
if (mCarWatchdogManager == null) {
LOG.e("CarWatchdogManager is null. Could not set killable state for '" + mPackageName
+ "'.");
connectToCar();
return false;
}
return true;
}
private void connectToCar() {
if (mCar != null && mCar.isConnected()) {
mCar.disconnect();
mCar = null;
}
mCar = Car.createCar(getContext(), null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
(car, isReady) -> {
mCarWatchdogManager = isReady
? (CarWatchdogManager) car.getCarManager(Car.CAR_WATCHDOG_SERVICE)
: null;
});
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.app.usage.UsageStats;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import androidx.preference.PreferenceGroup;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import java.util.List;
/**
* Class that controls visibility based on whether there have been recently used apps.
* Hidden if there have are no recently used apps.
*/
public class RecentAppsGroupPreferenceController extends PreferenceController<PreferenceGroup>
implements RecentAppsItemManager.RecentAppStatsListener {
// In most cases, device has recently opened apps. So, assume true by default.
private boolean mAreThereRecentlyUsedApps = true;
public RecentAppsGroupPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<PreferenceGroup> getPreferenceType() {
return PreferenceGroup.class;
}
@Override
public int getDefaultAvailabilityStatus() {
return mAreThereRecentlyUsedApps ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
}
@Override
public void onRecentAppStatsLoaded(List<UsageStats> recentAppStats) {
mAreThereRecentlyUsedApps = !recentAppStats.isEmpty();
refreshUi();
}
}

View File

@@ -0,0 +1,199 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.app.Application;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.car.settings.R;
import com.android.car.settings.common.Logger;
import com.android.settingslib.applications.AppUtils;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* Class for fetching and returning recently used apps. Largely derived from
* {@link com.android.settings.applications.RecentAppStatsMixin}.
*/
public class RecentAppsItemManager implements Comparator<UsageStats> {
private static final Logger LOG = new Logger(RecentAppsItemManager.class);
@VisibleForTesting
final List<UsageStats> mRecentApps;
private final int mUserId;
private final int mMaximumApps;
private final Context mContext;
private final PackageManager mPm;
private final UsageStatsManager mUsageStatsManager;
private final ApplicationsState mApplicationsState;
private final SparseArray<RecentAppStatsListener> mAppStatsListeners;
private final int mDaysThreshold;
private final List<String> mIgnoredPackages;
private Calendar mCalendar;
public RecentAppsItemManager(Context context, int maximumApps) {
this(context, maximumApps, ApplicationsState.getInstance(
(Application) context.getApplicationContext()));
}
@VisibleForTesting
RecentAppsItemManager(Context context, int maximumApps, ApplicationsState applicationsState) {
mContext = context;
mMaximumApps = maximumApps;
mUserId = UserHandle.myUserId();
mPm = mContext.getPackageManager();
mUsageStatsManager = mContext.getSystemService(UsageStatsManager.class);
mApplicationsState = applicationsState;
mRecentApps = new ArrayList<>();
mAppStatsListeners = new SparseArray<>();
mDaysThreshold = mContext.getResources()
.getInteger(R.integer.recent_apps_days_threshold);
mIgnoredPackages = Arrays.asList(mContext.getResources()
.getStringArray(R.array.recent_apps_ignored_packages));
}
/**
* Starts fetching recently used apps
*/
public void startLoading() {
ThreadUtils.postOnBackgroundThread(() -> {
loadDisplayableRecentApps(mMaximumApps);
for (int i = 0; i < mAppStatsListeners.size(); i++) {
int finalIndex = i;
ThreadUtils.postOnMainThread(() -> mAppStatsListeners.valueAt(finalIndex)
.onRecentAppStatsLoaded(mRecentApps));
}
});
}
@Override
public final int compare(UsageStats a, UsageStats b) {
// return by descending order
return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed());
}
/**
* Registers a listener that will be notified once the data is loaded.
*/
public void addListener(@NonNull RecentAppStatsListener listener) {
mAppStatsListeners.append(mAppStatsListeners.size(), listener);
}
@VisibleForTesting
void loadDisplayableRecentApps(int number) {
mRecentApps.clear();
mCalendar = Calendar.getInstance();
mCalendar.add(Calendar.DAY_OF_YEAR, -mDaysThreshold);
List<UsageStats> mStats = mUsageStatsManager.queryUsageStats(
UsageStatsManager.INTERVAL_BEST, mCalendar.getTimeInMillis(),
System.currentTimeMillis());
Map<String, UsageStats> map = new ArrayMap<>();
for (UsageStats pkgStats : mStats) {
if (!shouldIncludePkgInRecents(pkgStats)) {
continue;
}
String pkgName = pkgStats.getPackageName();
UsageStats existingStats = map.get(pkgName);
if (existingStats == null) {
map.put(pkgName, pkgStats);
} else {
existingStats.add(pkgStats);
}
}
List<UsageStats> packageStats = new ArrayList<>();
packageStats.addAll(map.values());
Collections.sort(packageStats, /* comparator= */ this);
int count = 0;
for (UsageStats stat : packageStats) {
ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry(
stat.getPackageName(), mUserId);
if (appEntry == null) {
continue;
}
mRecentApps.add(stat);
count++;
if (count >= number) {
break;
}
}
}
/**
* Whether or not the app should be included in recent list.
*/
private boolean shouldIncludePkgInRecents(UsageStats stat) {
String pkgName = stat.getPackageName();
if (stat.getLastTimeUsed() < mCalendar.getTimeInMillis()) {
LOG.d("Invalid timestamp (usage time is more than 24 hours ago), skipping "
+ pkgName);
return false;
}
if (mIgnoredPackages.contains(pkgName)) {
LOG.d("System package, skipping " + pkgName);
return false;
}
if (AppUtils.isHiddenSystemModule(mContext, pkgName)) {
return false;
}
Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER)
.setPackage(pkgName);
if (mPm.resolveActivity(launchIntent, 0) == null) {
// Not visible on launcher -> likely not a user visible app, skip if non-instant.
ApplicationsState.AppEntry appEntry =
mApplicationsState.getEntry(pkgName, mUserId);
if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) {
LOG.d("Not a user visible or instant app, skipping " + pkgName);
return false;
}
}
return true;
}
/**
* Callback that is called once the recently used apps have been fetched.
*/
public interface RecentAppStatsListener {
/**
* Called when the recently used apps are successfully loaded
*/
void onRecentAppStatsLoaded(List<UsageStats> recentAppStats);
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.app.Application;
import android.app.usage.UsageStats;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.os.UserHandle;
import android.text.format.DateUtils;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.car.ui.preference.CarUiPreference;
import com.android.settingslib.Utils;
import com.android.settingslib.applications.ApplicationsState;
import java.util.ArrayList;
import java.util.List;
/**
* Class responsible for displaying recently used apps.
*/
public class RecentAppsListPreferenceController extends PreferenceController<PreferenceCategory>
implements RecentAppsItemManager.RecentAppStatsListener {
private ApplicationsState mApplicationsState;
private int mUserId;
private List<UsageStats> mRecentAppStats;
private int mMaxRecentAppsCount;
public RecentAppsListPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
this(context, preferenceKey, fragmentController, uxRestrictions, ApplicationsState
.getInstance((Application) context.getApplicationContext()));
}
@VisibleForTesting
RecentAppsListPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions,
ApplicationsState applicationsState) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mApplicationsState = applicationsState;
mUserId = UserHandle.myUserId();
mRecentAppStats = new ArrayList<>();
mMaxRecentAppsCount = getContext().getResources().getInteger(
R.integer.recent_apps_max_count);
}
@Override
public void onRecentAppStatsLoaded(List<UsageStats> recentAppStats) {
mRecentAppStats = recentAppStats;
refreshUi();
}
@Override
protected Class<PreferenceCategory> getPreferenceType() {
return PreferenceCategory.class;
}
@Override
protected void updateState(PreferenceCategory preferenceCategory) {
preferenceCategory.setVisible(!mRecentAppStats.isEmpty());
preferenceCategory.removeAll();
int prefCount = 0;
for (UsageStats usageStats : mRecentAppStats) {
Preference pref = createPreference(getContext(), usageStats);
if (pref != null) {
getPreference().addPreference(pref);
prefCount++;
if (prefCount == mMaxRecentAppsCount) {
break;
}
}
}
}
private Preference createPreference(Context context, UsageStats usageStats) {
String pkgName = usageStats.getPackageName();
ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry(pkgName, mUserId);
if (appEntry == null) {
return null;
}
Preference pref = new CarUiPreference(context);
pref.setTitle(appEntry.label);
if (appEntry.icon == null) {
pref.setIcon(Utils.getBadgedIcon(context, appEntry.info));
} else {
pref.setIcon(appEntry.icon);
}
pref.setSummary(DateUtils.getRelativeTimeSpanString(usageStats.getLastTimeStamp(),
System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS));
pref.setOnPreferenceClickListener(p -> {
getFragmentController().launchFragment(
ApplicationDetailsFragment.getInstance(pkgName));
return true;
});
return pref;
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* Class responsible for directing users to view the apps list page.
* Sets title based on number of installed non-system apps.
*/
public class RecentAppsViewAllPreferenceController extends PreferenceController<Preference>
implements InstalledAppCountItemManager.InstalledAppCountListener {
public RecentAppsViewAllPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
@Override
public void onInstalledAppCountLoaded(int appCount) {
getPreference().setTitle(getContext().getResources().getString(
R.string.apps_view_all_apps_title, appCount));
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.os.UserHandle;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.car.settings.storage.AppStorageSettingsDetailsFragment;
import com.android.settingslib.applications.ApplicationsState;
import java.util.ArrayList;
/** Business logic for the storage entry in the application details settings. */
public class StoragePreferenceController extends PreferenceController<Preference> {
private ApplicationsState mApplicationsState;
private ApplicationsState.AppEntry mAppEntry;
private ApplicationsState.Session mSession;
private String mPackageName;
@VisibleForTesting
final ApplicationsState.Callbacks mApplicationStateCallbacks =
new ApplicationsState.Callbacks() {
@Override
public void onRunningStateChanged(boolean running) {
}
@Override
public void onPackageListChanged() {
}
@Override
public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
}
@Override
public void onPackageIconChanged() {
}
@Override
public void onPackageSizeChanged(String packageName) {
if (packageName.equals(mPackageName)) {
refreshUi();
}
}
@Override
public void onAllSizesComputed() {
refreshUi();
}
@Override
public void onLauncherInfoChanged() {
}
@Override
public void onLoadEntriesCompleted() {
}
};
public StoragePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
/** Sets the {@link ApplicationsState.AppEntry} which is used to load the app size. */
public StoragePreferenceController setAppEntry(ApplicationsState.AppEntry appEntry) {
mAppEntry = appEntry;
return this;
}
/** Sets the {@link ApplicationsState} which is used to load the app size. */
public StoragePreferenceController setAppState(ApplicationsState applicationsState) {
mApplicationsState = applicationsState;
return this;
}
/**
* Set the packageName, which is used to open the AppStorageSettingsDetailsFragment
*/
public StoragePreferenceController setPackageName(String packageName) {
mPackageName = packageName;
return this;
}
@Override
protected void checkInitialized() {
if (mAppEntry == null || mApplicationsState == null || mPackageName == null) {
throw new IllegalStateException(
"AppEntry, ApplicationsState and PackageName should be set before calling this "
+ "function");
}
}
@Override
protected void onCreateInternal() {
mSession = mApplicationsState.newSession(mApplicationStateCallbacks);
}
@Override
protected void onStartInternal() {
mSession.onResume();
}
@Override
protected void onStopInternal() {
mSession.onPause();
}
@Override
protected void updateState(Preference preference) {
refreshAppEntry();
if (mAppEntry == null) {
getFragmentController().goBack();
} else if (mAppEntry.sizeStr == null) {
preference.setSummary(
getContext().getString(R.string.memory_calculating_size));
} else {
preference.setSummary(
getContext().getString(R.string.storage_type_internal, mAppEntry.sizeStr));
}
}
@Override
protected boolean handlePreferenceClicked(Preference preference) {
getFragmentController().launchFragment(
AppStorageSettingsDetailsFragment.getInstance(mPackageName));
return true;
}
// TODO(b/201351382): Remove after SettingsLib investigation
private void refreshAppEntry() {
mAppEntry = mApplicationsState.getEntry(mPackageName, UserHandle.myUserId());
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.pm.PackageInfo;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/** Business logic for the Version field in the application details page. */
public class VersionPreferenceController extends PreferenceController<Preference> {
private PackageInfo mPackageInfo;
public VersionPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
/** Set the package info which is used to get the version name. */
public void setPackageInfo(PackageInfo packageInfo) {
mPackageInfo = packageInfo;
}
@Override
protected void checkInitialized() {
if (mPackageInfo == null) {
throw new IllegalStateException(
"PackageInfo should be set before calling this function");
}
}
@Override
protected void updateState(Preference preference) {
preference.setTitle(getContext().getString(
R.string.application_version_label, mPackageInfo.versionName));
}
@Override
protected int getDefaultAvailabilityStatus() {
return AVAILABLE_FOR_VIEWING;
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.appinfo
import android.car.drivingstate.CarUxRestrictions
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.content.res.Resources
import android.location.LocationManager
import androidx.preference.Preference
import com.android.car.settings.common.FragmentController
import com.android.car.settings.common.Logger
import com.android.car.settings.common.PreferenceController
/** Preference Controller for the "All Services" preference in the "App Info" page. */
class AppAllServicesPreferenceController(
context: Context,
preferenceKey: String,
fragmentController: FragmentController,
uxRestrictions: CarUxRestrictions
) : PreferenceController<Preference>(context, preferenceKey, fragmentController, uxRestrictions) {
private val packageManager = context.packageManager
private var packageName: String? = null
override fun onStartInternal() {
getStorageSummary()?.let { preference.summary = it }
}
override fun getPreferenceType(): Class<Preference> = Preference::class.java
override fun getDefaultAvailabilityStatus(): Int {
return if (canPackageHandleIntent() && isLocationProvider()) {
AVAILABLE
} else {
CONDITIONALLY_UNAVAILABLE
}
}
override fun handlePreferenceClicked(preference: Preference): Boolean {
startAllServicesActivity()
return true
}
/**
* Set the package name of the package for which the "All Services" activity needs to be shown.
*
* @param packageName Name of the package for which the services need to be shown.
*/
fun setPackageName(packageName: String?) {
this.packageName = packageName
}
private fun getStorageSummary(): CharSequence? {
val resolveInfo = getResolveInfo(PackageManager.GET_META_DATA)
if (resolveInfo == null) {
LOG.d("mResolveInfo is null.")
return null
}
val metaData = resolveInfo.activityInfo.metaData
try {
val pkgRes = packageManager.getResourcesForActivity(
ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name))
return pkgRes.getString(metaData.getInt(SUMMARY_METADATA_KEY))
} catch (exception: Resources.NotFoundException) {
LOG.d("Resource not found for summary string.")
} catch (exception: PackageManager.NameNotFoundException) {
LOG.d("Name of resource not found for summary string.")
}
return null
}
private fun isLocationProvider(): Boolean {
val locationManagerService = context.getSystemService(LocationManager::class.java)
return packageName?.let {
locationManagerService?.isProviderPackage(
/* provider = */ null,
/* packageName = */ it,
/* attributionTag = */ null)
} ?: false
}
private fun canPackageHandleIntent(): Boolean = getResolveInfo(/* flags = */ 0) != null
private fun startAllServicesActivity() {
val featuresIntent = Intent(Intent.ACTION_VIEW_APP_FEATURES)
// Resolve info won't be null as it is only shown for packages that can
// handle the intent
val resolveInfo = getResolveInfo(/* flags = */ 0)
if (resolveInfo == null) {
LOG.e("Resolve info is null, package unable to handle all services intent")
return
}
featuresIntent.component =
ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name)
LOG.v("Starting the All Services activity with intent:" +
featuresIntent.toUri(/* flags = */ 0))
try {
context.startActivity(featuresIntent)
} catch (e: ActivityNotFoundException) {
LOG.e("The app cannot handle android.intent.action.VIEW_APP_FEATURES")
}
}
private fun getResolveInfo(flags: Int): ResolveInfo? {
if (packageName == null) {
return null
}
val featuresIntent = Intent(Intent.ACTION_VIEW_APP_FEATURES).apply {
`package` = packageName
}
return packageManager.resolveActivity(featuresIntent, flags)
}
private companion object {
val LOG = Logger(AppAllServicesPreferenceController::class.java)
const val SUMMARY_METADATA_KEY = "app_features_preference_summary"
}
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.appinfo;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_DEFAULT;
import static android.app.AppOpsManager.MODE_IGNORED;
import static android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED;
import static com.android.car.settings.applications.ApplicationsUtils.isHibernationEnabled;
import android.app.AppOpsManager;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import android.util.Slog;
import androidx.preference.TwoStatePreference;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
/**
* A PreferenceController handling the logic for exempting hibernation of app
*/
public final class HibernationSwitchPreferenceController
extends PreferenceController<TwoStatePreference>
implements AppOpsManager.OnOpChangedListener {
private static final String TAG = "HibernationSwitchPrefController";
private String mPackageName;
private final AppOpsManager mAppOpsManager;
private int mPackageUid;
private boolean mIsPackageSet;
private boolean mIsPackageExemptByDefault;
public HibernationSwitchPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mAppOpsManager = context.getSystemService(AppOpsManager.class);
}
@Override
protected Class<TwoStatePreference> getPreferenceType() {
return TwoStatePreference.class;
}
@Override
public int getDefaultAvailabilityStatus() {
return isHibernationEnabled() && mIsPackageSet ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
}
@Override
protected void onStartInternal() {
if (mIsPackageSet) {
mAppOpsManager.startWatchingMode(
OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageName, this);
}
}
@Override
protected void onStopInternal() {
mAppOpsManager.stopWatchingMode(this);
}
/**
* Set the package. And also retrieve details from package manager. Some packages may be
* exempted from hibernation by default. This method should only be called to initialize the
* controller.
* @param packageName The name of the package whose hibernation state to be managed.
*/
public void setPackageName(String packageName) {
mPackageName = packageName;
PackageManager packageManager = getContext().getPackageManager();
// Q- packages exempt by default, except R- on Auto since Auto-Revoke was skipped in R
int maxTargetSdkVersionForExemptApps = android.os.Build.VERSION_CODES.R;
try {
mPackageUid = packageManager.getPackageUid(packageName, /* flags= */ 0);
mIsPackageExemptByDefault = packageManager.getTargetSdkVersion(packageName)
<= maxTargetSdkVersionForExemptApps;
mIsPackageSet = true;
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, "Package [" + mPackageName + "] is not found!");
mIsPackageSet = false;
}
}
@Override
protected void updateState(TwoStatePreference preference) {
super.updateState(preference);
preference.setChecked(!isPackageHibernationExemptByUser());
}
private boolean isPackageHibernationExemptByUser() {
if (!mIsPackageSet) return true;
int mode = mAppOpsManager.unsafeCheckOpNoThrow(
OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageUid, mPackageName);
return mode == MODE_DEFAULT ? mIsPackageExemptByDefault : mode != MODE_ALLOWED;
}
@Override
public void onOpChanged(String op, String packageName) {
if (OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED.equals(op)
&& TextUtils.equals(mPackageName, packageName)) {
refreshUi();
}
}
@Override
protected boolean handlePreferenceChanged(TwoStatePreference preference, Object newValue) {
try {
mAppOpsManager.setUidMode(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageUid,
(boolean) newValue ? MODE_ALLOWED : MODE_IGNORED);
} catch (RuntimeException e) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.assist;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.provider.Settings;
import androidx.annotation.VisibleForTesting;
import androidx.preference.TwoStatePreference;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.internal.app.AssistUtils;
import java.util.List;
/** Common logic for preference controllers that configure the assistant's behavior. */
public abstract class AssistConfigBasePreferenceController extends
PreferenceController<TwoStatePreference> {
final SettingObserver mSettingObserver;
private final AssistUtils mAssistUtils;
public AssistConfigBasePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
this(context, preferenceKey, fragmentController, uxRestrictions, new AssistUtils(context));
}
@VisibleForTesting
AssistConfigBasePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions,
AssistUtils assistUtils) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mAssistUtils = assistUtils;
mSettingObserver = new SettingObserver(getSettingUris(), this::refreshUi);
}
@Override
protected Class<TwoStatePreference> getPreferenceType() {
return TwoStatePreference.class;
}
@Override
protected int getDefaultAvailabilityStatus() {
return mAssistUtils.getAssistComponentForUser(
UserHandle.myUserId()) != null ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
}
@Override
protected void onStartInternal() {
mSettingObserver.register(getContext().getContentResolver(), true);
}
@Override
protected void onStopInternal() {
mSettingObserver.register(getContext().getContentResolver(), false);
}
/** Gets the Setting Uris that should be observed */
protected abstract List<Uri> getSettingUris();
/**
* Creates an observer that listens for changes to {@link Settings.Secure#ASSISTANT} as well as
* any other URI defined by {@link #getSettingUris()}.
*/
@VisibleForTesting
static class SettingObserver extends ContentObserver {
private static final Uri ASSIST_URI = Settings.Secure.getUriFor(Settings.Secure.ASSISTANT);
private final List<Uri> mUriList;
private final Runnable mSettingChangeListener;
SettingObserver(List<Uri> uriList, Runnable settingChangeListener) {
super(new Handler(Looper.getMainLooper()));
mUriList = uriList;
mSettingChangeListener = settingChangeListener;
}
/** Registers or unregisters this observer to the given content resolver. */
void register(ContentResolver cr, boolean register) {
if (register) {
cr.registerContentObserver(ASSIST_URI, /* notifyForDescendants= */ false,
/* observer= */ this);
if (mUriList != null) {
for (Uri uri : mUriList) {
cr.registerContentObserver(uri, /* notifyForDescendants= */ false,
/* observer= */ this);
}
}
} else {
cr.unregisterContentObserver(this);
}
}
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
if (shouldUpdatePreference(uri)) {
mSettingChangeListener.run();
}
}
private boolean shouldUpdatePreference(Uri uri) {
return ASSIST_URI.equals(uri) || (mUriList != null && mUriList.contains(uri));
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.assist;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ComponentName;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import com.android.car.settings.applications.defaultapps.DefaultAppEntryBasePreferenceController;
import com.android.car.settings.common.FragmentController;
import com.android.internal.app.AssistUtils;
import com.android.settingslib.applications.DefaultAppInfo;
/**
* Business logic to show the currently selected default assist.
*/
public class AssistantAndVoiceEntryPreferenceController extends
DefaultAppEntryBasePreferenceController<Preference> {
private final AssistUtils mAssistUtils;
public AssistantAndVoiceEntryPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mAssistUtils = new AssistUtils(context);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
@Nullable
protected DefaultAppInfo getCurrentDefaultAppInfo() {
ComponentName cn = mAssistUtils.getAssistComponentForUser(getCurrentProcessUserId());
if (cn == null) {
return null;
}
return new DefaultAppInfo(getContext(), getContext().getPackageManager(),
getCurrentProcessUserId(), cn);
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.assist;
import android.provider.Settings;
import androidx.annotation.XmlRes;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
import com.android.car.settings.search.CarBaseSearchIndexProvider;
import com.android.settingslib.search.SearchIndexable;
/** Assistant management settings screen. */
@SearchIndexable
public class AssistantAndVoiceFragment extends SettingsFragment {
@Override
@XmlRes
protected int getPreferenceScreenResId() {
return R.xml.assistant_and_voice_fragment;
}
/**
* Data provider for Settings Search.
*/
public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new CarBaseSearchIndexProvider(R.xml.assistant_and_voice_fragment,
Settings.ACTION_VOICE_INPUT_SETTINGS);
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.assist;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.car.settings.applications.defaultapps.DefaultAppsPickerEntryBasePreferenceController;
import com.android.car.settings.common.FragmentController;
import com.android.internal.app.AssistUtils;
import com.android.settingslib.applications.DefaultAppInfo;
import java.util.Objects;
/**
* Business logic to show the currently selected default voice input service and also link to the
* service settings, if it exists.
*/
public class DefaultVoiceInputPickerEntryPreferenceController extends
DefaultAppsPickerEntryBasePreferenceController {
private static final Uri ASSIST_URI = Settings.Secure.getUriFor(Settings.Secure.ASSISTANT);
@VisibleForTesting
final ContentObserver mSettingObserver = new ContentObserver(
new Handler(Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
if (ASSIST_URI.equals(uri)) {
refreshUi();
}
}
};
private final VoiceInputInfoProvider mVoiceInputInfoProvider;
private final AssistUtils mAssistUtils;
public DefaultVoiceInputPickerEntryPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
this(context, preferenceKey, fragmentController, uxRestrictions,
new VoiceInputInfoProvider(context), new AssistUtils(context));
}
@VisibleForTesting
DefaultVoiceInputPickerEntryPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions,
VoiceInputInfoProvider voiceInputInfoProvider, AssistUtils assistUtils) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mVoiceInputInfoProvider = voiceInputInfoProvider;
mAssistUtils = assistUtils;
}
@Override
protected int getDefaultAvailabilityStatus() {
ComponentName currentVoiceService = VoiceInputUtils.getCurrentService(getContext());
ComponentName currentAssist = mAssistUtils.getAssistComponentForUser(
getCurrentProcessUserId());
return Objects.equals(currentAssist, currentVoiceService) ? CONDITIONALLY_UNAVAILABLE
: AVAILABLE;
}
@Override
protected void onStartInternal() {
getContext().getContentResolver().registerContentObserver(ASSIST_URI,
/* notifyForDescendants= */ false, mSettingObserver);
}
@Override
protected void onStopInternal() {
getContext().getContentResolver().unregisterContentObserver(mSettingObserver);
}
@Nullable
@Override
protected DefaultAppInfo getCurrentDefaultAppInfo() {
VoiceInputInfoProvider.VoiceInputInfo info = mVoiceInputInfoProvider.getInfoForComponent(
VoiceInputUtils.getCurrentService(getContext()));
return (info == null) ? null : new DefaultVoiceInputServiceInfo(getContext(),
getContext().getPackageManager(), getCurrentProcessUserId(), info,
/* enabled= */ true);
}
@Nullable
@Override
protected Intent getSettingIntent(@Nullable DefaultAppInfo info) {
if (info instanceof DefaultVoiceInputServiceInfo) {
return ((DefaultVoiceInputServiceInfo) info).getSettingIntent();
}
return null;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.assist;
import androidx.annotation.XmlRes;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
/** Shows the screen to pick the default voice input service. */
public class DefaultVoiceInputPickerFragment extends SettingsFragment {
@Override
@XmlRes
protected int getPreferenceScreenResId() {
return R.xml.default_voice_input_picker_fragment;
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.assist;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ComponentName;
import android.content.Context;
import android.provider.Settings;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.car.settings.applications.defaultapps.DefaultAppsPickerBasePreferenceController;
import com.android.car.settings.common.FragmentController;
import com.android.internal.app.AssistUtils;
import com.android.settingslib.applications.DefaultAppInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/** Business logic for displaying and choosing the default voice input service. */
public class DefaultVoiceInputPickerPreferenceController extends
DefaultAppsPickerBasePreferenceController {
private final AssistUtils mAssistUtils;
private final VoiceInputInfoProvider mVoiceInputInfoProvider;
// Current assistant component name, used to restrict available voice inputs.
private String mAssistComponentName = null;
public DefaultVoiceInputPickerPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
this(context, preferenceKey, fragmentController, uxRestrictions, new AssistUtils(context),
new VoiceInputInfoProvider(context));
}
@VisibleForTesting
DefaultVoiceInputPickerPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions,
AssistUtils assistUtils, VoiceInputInfoProvider voiceInputInfoProvider) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mAssistUtils = assistUtils;
mVoiceInputInfoProvider = voiceInputInfoProvider;
if (Objects.equals(mAssistUtils.getAssistComponentForUser(getCurrentProcessUserId()),
VoiceInputUtils.getCurrentService(getContext()))) {
ComponentName cn = mAssistUtils.getAssistComponentForUser(getCurrentProcessUserId());
if (cn != null) {
mAssistComponentName = cn.flattenToString();
}
}
}
@NonNull
@Override
protected List<DefaultAppInfo> getCandidates() {
List<DefaultAppInfo> candidates = new ArrayList<>();
for (VoiceInputInfoProvider.VoiceInteractionInfo info :
mVoiceInputInfoProvider.getVoiceInteractionInfoList()) {
boolean enabled = TextUtils.equals(info.getComponentName().flattenToString(),
mAssistComponentName);
candidates.add(
new DefaultVoiceInputServiceInfo(getContext(), getContext().getPackageManager(),
getCurrentProcessUserId(), info, enabled));
}
for (VoiceInputInfoProvider.VoiceRecognitionInfo info :
mVoiceInputInfoProvider.getVoiceRecognitionInfoList()) {
candidates.add(
new DefaultVoiceInputServiceInfo(getContext(), getContext().getPackageManager(),
getCurrentProcessUserId(), info, /* enabled= */ true));
}
return candidates;
}
@Override
protected String getCurrentDefaultKey() {
ComponentName cn = VoiceInputUtils.getCurrentService(getContext());
if (cn == null) {
return null;
}
return cn.flattenToString();
}
@Override
protected void setCurrentDefault(String key) {
ComponentName cn = ComponentName.unflattenFromString(key);
VoiceInputInfoProvider.VoiceInputInfo info = mVoiceInputInfoProvider.getInfoForComponent(
cn);
if (info instanceof VoiceInputInfoProvider.VoiceInteractionInfo) {
VoiceInputInfoProvider.VoiceInteractionInfo interactionInfo =
(VoiceInputInfoProvider.VoiceInteractionInfo) info;
Settings.Secure.putString(getContext().getContentResolver(),
Settings.Secure.VOICE_INTERACTION_SERVICE, key);
Settings.Secure.putString(getContext().getContentResolver(),
Settings.Secure.VOICE_RECOGNITION_SERVICE,
new ComponentName(interactionInfo.getPackageName(),
interactionInfo.getRecognitionService())
.flattenToString());
} else if (info instanceof VoiceInputInfoProvider.VoiceRecognitionInfo) {
Settings.Secure.putString(getContext().getContentResolver(),
Settings.Secure.VOICE_INTERACTION_SERVICE, "");
Settings.Secure.putString(getContext().getContentResolver(),
Settings.Secure.VOICE_RECOGNITION_SERVICE, key);
}
}
@Override
protected boolean includeNonePreference() {
return false;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.assist;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import androidx.annotation.Nullable;
import com.android.car.settings.applications.assist.VoiceInputInfoProvider.VoiceInteractionInfo;
import com.android.car.settings.applications.assist.VoiceInputInfoProvider.VoiceRecognitionInfo;
import com.android.settingslib.applications.DefaultAppInfo;
/** An extension of {@link DefaultAppInfo} to help represent voice input services. */
public class DefaultVoiceInputServiceInfo extends DefaultAppInfo {
private VoiceInputInfoProvider.VoiceInputInfo mInfo;
/**
* Constructs a {@link DefaultVoiceInputServiceInfo}
*
* @param info a {@link VoiceInteractionInfo} or {@link VoiceRecognitionInfo} that describes
* the Voice Input Service.
* @param enabled determines whether the service should be selectable or not.
*/
public DefaultVoiceInputServiceInfo(Context context, PackageManager pm, int userId,
VoiceInputInfoProvider.VoiceInputInfo info, boolean enabled) {
super(context, pm, userId, info.getComponentName(), /* summary= */ null, enabled);
mInfo = info;
}
@Override
public CharSequence loadLabel() {
return mInfo.getLabel();
}
/** Gets the intent to open the related settings component if it exists. */
@Nullable
public Intent getSettingIntent() {
if (mInfo.getSettingsActivityComponentName() == null) {
return null;
}
return new Intent(Intent.ACTION_MAIN).setComponent(
mInfo.getSettingsActivityComponentName());
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.assist;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.net.Uri;
import android.provider.Settings;
import androidx.preference.TwoStatePreference;
import com.android.car.settings.common.FragmentController;
import java.util.Arrays;
import java.util.List;
/** Toggles the assistant's ability to use a screenshot of the screen for context. */
public class ScreenshotContextPreferenceController extends AssistConfigBasePreferenceController {
public ScreenshotContextPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected void updateState(TwoStatePreference preference) {
boolean checked = Settings.Secure.getInt(getContext().getContentResolver(),
Settings.Secure.ASSIST_SCREENSHOT_ENABLED, 1) != 0;
preference.setChecked(checked);
boolean contextChecked = Settings.Secure.getInt(getContext().getContentResolver(),
Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1) != 0;
preference.setEnabled(contextChecked);
}
@Override
protected boolean handlePreferenceChanged(TwoStatePreference preference, Object newValue) {
Settings.Secure.putInt(getContext().getContentResolver(),
Settings.Secure.ASSIST_SCREENSHOT_ENABLED, (boolean) newValue ? 1 : 0);
return true;
}
@Override
protected List<Uri> getSettingUris() {
return Arrays.asList(
Settings.Secure.getUriFor(Settings.Secure.ASSIST_SCREENSHOT_ENABLED),
Settings.Secure.getUriFor(Settings.Secure.ASSIST_STRUCTURE_ENABLED));
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.assist;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.net.Uri;
import android.provider.Settings;
import androidx.preference.TwoStatePreference;
import com.android.car.settings.common.FragmentController;
import java.util.Collections;
import java.util.List;
/** Toggles the assistant's ability to use the text on the screen for context. */
public class TextContextPreferenceController extends AssistConfigBasePreferenceController {
public TextContextPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected void updateState(TwoStatePreference preference) {
preference.setChecked(isAssistContextEnabled());
}
@Override
protected boolean handlePreferenceChanged(TwoStatePreference preference, Object newValue) {
Settings.Secure.putInt(getContext().getContentResolver(),
Settings.Secure.ASSIST_STRUCTURE_ENABLED, (boolean) newValue ? 1 : 0);
return true;
}
@Override
protected List<Uri> getSettingUris() {
return Collections.singletonList(
Settings.Secure.getUriFor(Settings.Secure.ASSIST_STRUCTURE_ENABLED));
}
private boolean isAssistContextEnabled() {
return Settings.Secure.getInt(getContext().getContentResolver(),
Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1) != 0;
}
}

View File

@@ -0,0 +1,310 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.assist;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.service.voice.VoiceInteractionService;
import android.service.voice.VoiceInteractionServiceInfo;
import android.speech.RecognitionService;
import android.util.AttributeSet;
import android.util.Xml;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import com.android.car.settings.common.Logger;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Extracts the voice interaction services and voice recognition services and converts them into
* {@link VoiceInteractionInfo} instances and {@link VoiceRecognitionInfo} instances.
*/
public class VoiceInputInfoProvider {
private static final Logger LOG = new Logger(VoiceInputInfoProvider.class);
@VisibleForTesting
static final Intent VOICE_INTERACTION_SERVICE_TAG = new Intent(
VoiceInteractionService.SERVICE_INTERFACE);
@VisibleForTesting
static final Intent VOICE_RECOGNITION_SERVICE_TAG = new Intent(
RecognitionService.SERVICE_INTERFACE);
private final Context mContext;
private final Map<ComponentName, VoiceInputInfo> mComponentToInfoMap = new ArrayMap<>();
private final List<VoiceInteractionInfo> mVoiceInteractionInfoList = new ArrayList<>();
private final List<VoiceRecognitionInfo> mVoiceRecognitionInfoList = new ArrayList<>();
private final Set<ComponentName> mRecognitionServiceNames = new ArraySet<>();
public VoiceInputInfoProvider(Context context) {
mContext = context;
loadVoiceInteractionServices();
loadVoiceRecognitionServices();
}
/**
* Gets the list of voice interaction services represented as {@link VoiceInteractionInfo}
* instances.
*/
public List<VoiceInteractionInfo> getVoiceInteractionInfoList() {
return mVoiceInteractionInfoList;
}
/**
* Gets the list of voice recognition services represented as {@link VoiceRecognitionInfo}
* instances.
*/
public List<VoiceRecognitionInfo> getVoiceRecognitionInfoList() {
return mVoiceRecognitionInfoList;
}
/**
* Returns the appropriate {@link VoiceInteractionInfo} or {@link VoiceRecognitionInfo} based on
* the provided {@link ComponentName}.
*
* @return {@link VoiceInputInfo} if it exists for the component name, null otherwise.
*/
@Nullable
public VoiceInputInfo getInfoForComponent(ComponentName key) {
return mComponentToInfoMap.getOrDefault(key, null);
}
private void loadVoiceInteractionServices() {
List<ResolveInfo> mAvailableVoiceInteractionServices =
mContext.getPackageManager().queryIntentServices(VOICE_INTERACTION_SERVICE_TAG,
PackageManager.GET_META_DATA);
for (ResolveInfo resolveInfo : mAvailableVoiceInteractionServices) {
VoiceInteractionServiceInfo interactionServiceInfo = new VoiceInteractionServiceInfo(
mContext.getPackageManager(), resolveInfo.serviceInfo);
if (hasParseError(interactionServiceInfo)) {
LOG.w("Error in VoiceInteractionService " + resolveInfo.serviceInfo.packageName
+ "/" + resolveInfo.serviceInfo.name + ": "
+ interactionServiceInfo.getParseError());
continue;
}
VoiceInteractionInfo voiceInteractionInfo = new VoiceInteractionInfo(mContext,
interactionServiceInfo);
mVoiceInteractionInfoList.add(voiceInteractionInfo);
if (interactionServiceInfo.getRecognitionService() != null) {
mRecognitionServiceNames.add(new ComponentName(resolveInfo.serviceInfo.packageName,
interactionServiceInfo.getRecognitionService()));
}
mComponentToInfoMap.put(new ComponentName(resolveInfo.serviceInfo.packageName,
resolveInfo.serviceInfo.name), voiceInteractionInfo);
}
Collections.sort(mVoiceInteractionInfoList);
}
private void loadVoiceRecognitionServices() {
List<ResolveInfo> mAvailableRecognitionServices =
mContext.getPackageManager().queryIntentServices(VOICE_RECOGNITION_SERVICE_TAG,
PackageManager.GET_META_DATA);
for (ResolveInfo resolveInfo : mAvailableRecognitionServices) {
ComponentName componentName = new ComponentName(resolveInfo.serviceInfo.packageName,
resolveInfo.serviceInfo.name);
VoiceRecognitionInfo voiceRecognitionInfo = new VoiceRecognitionInfo(mContext,
resolveInfo.serviceInfo);
mVoiceRecognitionInfoList.add(voiceRecognitionInfo);
mRecognitionServiceNames.add(componentName);
mComponentToInfoMap.put(componentName, voiceRecognitionInfo);
}
Collections.sort(mVoiceRecognitionInfoList);
}
@VisibleForTesting
boolean hasParseError(VoiceInteractionServiceInfo voiceInteractionServiceInfo) {
return voiceInteractionServiceInfo.getParseError() != null;
}
/**
* Base object used to represent {@link VoiceInteractionInfo} and {@link VoiceRecognitionInfo}.
*/
abstract static class VoiceInputInfo implements Comparable<VoiceInputInfo> {
private final Context mContext;
private final ServiceInfo mServiceInfo;
VoiceInputInfo(Context context, ServiceInfo serviceInfo) {
mContext = context;
mServiceInfo = serviceInfo;
}
protected Context getContext() {
return mContext;
}
protected ServiceInfo getServiceInfo() {
return mServiceInfo;
}
@Override
public int compareTo(VoiceInputInfo o) {
return getTag().toString().compareTo(o.getTag().toString());
}
/**
* Returns the {@link ComponentName} which represents the settings activity, if it exists.
*/
@Nullable
ComponentName getSettingsActivityComponentName() {
String activity = getSettingsActivity();
return (activity != null) ? new ComponentName(mServiceInfo.packageName, activity)
: null;
}
/** Returns the package name for the service represented by this {@link VoiceInputInfo}. */
String getPackageName() {
return mServiceInfo.packageName;
}
/**
* Returns the component name for the service represented by this {@link VoiceInputInfo}.
*/
ComponentName getComponentName() {
return new ComponentName(mServiceInfo.packageName, mServiceInfo.name);
}
/**
* Returns the label to describe the service represented by this {@link VoiceInputInfo}.
*/
abstract CharSequence getLabel();
/**
* The string representation of the settings activity for the service represented by this
* {@link VoiceInputInfo}.
*/
protected abstract String getSettingsActivity();
/**
* Returns a tag used to determine the sort order of the {@link VoiceInputInfo} instances.
*/
protected CharSequence getTag() {
return mServiceInfo.loadLabel(mContext.getPackageManager());
}
}
/** An object to represent {@link VoiceInteractionService} instances. */
static class VoiceInteractionInfo extends VoiceInputInfo {
private final VoiceInteractionServiceInfo mInteractionServiceInfo;
VoiceInteractionInfo(Context context, VoiceInteractionServiceInfo info) {
super(context, info.getServiceInfo());
mInteractionServiceInfo = info;
}
/** Returns the recognition service associated with this {@link VoiceInteractionService}. */
String getRecognitionService() {
return mInteractionServiceInfo.getRecognitionService();
}
@Override
protected String getSettingsActivity() {
return mInteractionServiceInfo.getSettingsActivity();
}
@Override
CharSequence getLabel() {
return getServiceInfo().applicationInfo.loadLabel(getContext().getPackageManager());
}
}
/** An object to represent {@link RecognitionService} instances. */
static class VoiceRecognitionInfo extends VoiceInputInfo {
VoiceRecognitionInfo(Context context, ServiceInfo serviceInfo) {
super(context, serviceInfo);
}
@Override
protected String getSettingsActivity() {
return getServiceSettingsActivity(getServiceInfo());
}
@Override
CharSequence getLabel() {
return getTag();
}
private String getServiceSettingsActivity(ServiceInfo serviceInfo) {
XmlResourceParser parser = null;
String settingActivity = null;
try {
parser = serviceInfo.loadXmlMetaData(getContext().getPackageManager(),
RecognitionService.SERVICE_META_DATA);
if (parser == null) {
throw new XmlPullParserException(
"No " + RecognitionService.SERVICE_META_DATA + " meta-data for "
+ serviceInfo.packageName);
}
Resources res = getContext().getPackageManager().getResourcesForApplication(
serviceInfo.applicationInfo);
AttributeSet attrs = Xml.asAttributeSet(parser);
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& type != XmlPullParser.START_TAG) {
continue;
}
String nodeName = parser.getName();
if (!"recognition-service".equals(nodeName)) {
throw new XmlPullParserException(
"Meta-data does not start with recognition-service tag");
}
TypedArray array = res.obtainAttributes(attrs,
com.android.internal.R.styleable.RecognitionService);
settingActivity = array.getString(
com.android.internal.R.styleable.RecognitionService_settingsActivity);
array.recycle();
} catch (XmlPullParserException e) {
LOG.e("error parsing recognition service meta-data", e);
} catch (IOException e) {
LOG.e("error parsing recognition service meta-data", e);
} catch (PackageManager.NameNotFoundException e) {
LOG.e("error parsing recognition service meta-data", e);
} finally {
if (parser != null) parser.close();
}
return settingActivity;
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.assist;
import android.content.ComponentName;
import android.content.Context;
import android.provider.Settings;
import android.text.TextUtils;
/** Utilities to help interact with voice input services. */
final class VoiceInputUtils {
private VoiceInputUtils() {
}
/**
* Chooses the current service based on the current voice interaction service and current
* recognizer service.
*/
static ComponentName getCurrentService(Context context) {
ComponentName currentVoiceInteraction = getComponentNameOrNull(context,
Settings.Secure.VOICE_INTERACTION_SERVICE);
ComponentName currentVoiceRecognizer = getComponentNameOrNull(context,
Settings.Secure.VOICE_RECOGNITION_SERVICE);
if (currentVoiceInteraction != null) {
return currentVoiceInteraction;
} else if (currentVoiceRecognizer != null) {
return currentVoiceRecognizer;
} else {
return null;
}
}
private static ComponentName getComponentNameOrNull(Context context, String secureSettingKey) {
String currentSetting = Settings.Secure.getString(context.getContentResolver(),
secureSettingKey);
if (!TextUtils.isEmpty(currentSetting)) {
return ComponentName.unflattenFromString(currentSetting);
}
return null;
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.defaultapps;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.PreferenceController;
import com.android.car.ui.preference.CarUiPreference;
import com.android.settingslib.applications.DefaultAppInfo;
/**
* Base preference which handles the logic to display the currently selected default app.
*
* @param <V> the upper bound on the type of {@link Preference} on which the controller expects to
* operate.
*/
public abstract class DefaultAppEntryBasePreferenceController<V extends Preference> extends
PreferenceController<V> {
private static final Logger LOG = new Logger(
DefaultAppEntryBasePreferenceController.class);
public DefaultAppEntryBasePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected void updateState(V preference) {
CharSequence defaultAppLabel = getDefaultAppLabel();
if (!TextUtils.isEmpty(defaultAppLabel)) {
preference.setSummary(defaultAppLabel);
preference.setIcon(DefaultAppUtils.getSafeIcon(getDefaultAppIcon(),
getContext().getResources().getInteger(R.integer.default_app_safe_icon_size)));
} else {
LOG.d("No default app");
preference.setSummary(R.string.app_list_preference_none);
preference.setIcon(null);
if (preference instanceof CarUiPreference) {
((CarUiPreference) preference).setShowChevron(false);
}
}
}
/** Specifies the currently selected default app. */
@Nullable
protected abstract DefaultAppInfo getCurrentDefaultAppInfo();
/** Gets the current process user id. */
protected int getCurrentProcessUserId() {
return UserHandle.myUserId();
}
private Drawable getDefaultAppIcon() {
DefaultAppInfo app = getCurrentDefaultAppInfo();
if (app != null) {
return app.loadIcon();
}
return null;
}
private CharSequence getDefaultAppLabel() {
DefaultAppInfo app = getCurrentDefaultAppInfo();
if (app != null) {
return app.loadLabel();
}
return null;
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.defaultapps;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.VectorDrawable;
/** Utilities related to default apps. */
public class DefaultAppUtils {
private DefaultAppUtils() {
}
/** Scales the icon to a maximum size to avoid crashing Settings if it is too big. */
public static Drawable getSafeIcon(Drawable icon, int maxDimension) {
Drawable safeIcon = icon;
if ((icon != null) && !(icon instanceof VectorDrawable)) {
safeIcon = getSafeDrawable(icon, maxDimension);
}
return safeIcon;
}
/**
* Gets a drawable with a limited size to avoid crashing Settings if it's too big.
*
* @param original original drawable, typically an app icon.
* @param maxDimension maximum width/height, in pixels.
*/
private static Drawable getSafeDrawable(Drawable original, int maxDimension) {
int actualWidth = original.getMinimumWidth();
int actualHeight = original.getMinimumHeight();
if (actualWidth <= maxDimension && actualHeight <= maxDimension) {
return original;
}
float scaleWidth = ((float) maxDimension) / actualWidth;
float scaleHeight = ((float) maxDimension) / actualHeight;
float scale = Math.min(scaleWidth, scaleHeight);
int width = (int) (actualWidth * scale);
int height = (int) (actualHeight * scale);
Bitmap bitmap;
if (original instanceof BitmapDrawable) {
Bitmap originalBitmap = ((BitmapDrawable) original).getBitmap();
bitmap = Bitmap.createScaledBitmap(originalBitmap, width, height, false);
// When a new BitmapDrawable is created, it defaults to DisplayMetrics.DENSITY_DEFAULT
// density. Depending on the original bitmap density, this could increase the
// intrinsic height/width of the new BitmapDrawable.
BitmapDrawable scaledBitmap = new BitmapDrawable(/* res= */ null, bitmap);
scaledBitmap.setTargetDensity(originalBitmap.getDensity());
return scaledBitmap;
} else {
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
original.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
original.draw(canvas);
}
return new BitmapDrawable(/* res= */ null, bitmap);
}
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.defaultapps;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.os.UserHandle;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.preference.TwoStatePreference;
import com.android.car.settings.R;
import com.android.car.settings.common.ConfirmationDialogFragment;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.GroupSelectionPreferenceController;
import com.android.car.settings.common.Logger;
import com.android.car.ui.preference.CarUiRadioButtonPreference;
import com.android.settingslib.applications.DefaultAppInfo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Defines the shared logic in picking a default application. */
public abstract class DefaultAppsPickerBasePreferenceController extends
GroupSelectionPreferenceController {
private static final Logger LOG = new Logger(DefaultAppsPickerBasePreferenceController.class);
private static final String DIALOG_KEY_ARG = "key_arg";
protected static final String NONE_PREFERENCE_KEY = "";
private final Map<String, DefaultAppInfo> mDefaultAppInfoMap = new HashMap<>();
private final ConfirmationDialogFragment.ConfirmListener mConfirmListener = arguments -> {
setCurrentDefault(arguments.getString(DIALOG_KEY_ARG));
notifyCheckedKeyChanged();
};
public DefaultAppsPickerBasePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected void onCreateInternal() {
ConfirmationDialogFragment.resetListeners(
(ConfirmationDialogFragment) getFragmentController().findDialogByTag(
ConfirmationDialogFragment.TAG),
mConfirmListener,
/* rejectListener= */ null,
/* neutralListener= */ null);
}
@Override
@NonNull
protected List<TwoStatePreference> getGroupPreferences() {
List<TwoStatePreference> entries = new ArrayList<>();
if (includeNonePreference()) {
entries.add(createNoneOption());
}
List<DefaultAppInfo> currentCandidates = getCandidates();
if (currentCandidates != null) {
for (DefaultAppInfo info : currentCandidates) {
mDefaultAppInfoMap.put(info.getKey(), info);
entries.add(createOption(info));
}
} else {
LOG.i("no candidate provided");
}
return entries;
}
@Override
protected final boolean handleGroupItemSelected(TwoStatePreference preference) {
String selectedKey = preference.getKey();
if (TextUtils.equals(selectedKey, getCurrentCheckedKey())) {
return false;
}
CharSequence message = getConfirmationMessage(mDefaultAppInfoMap.get(selectedKey));
if (!TextUtils.isEmpty(message)) {
ConfirmationDialogFragment dialogFragment =
new ConfirmationDialogFragment.Builder(getContext())
.setMessage(message.toString())
.setPositiveButton(android.R.string.ok, mConfirmListener)
.setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
.addArgumentString(DIALOG_KEY_ARG, selectedKey)
.build();
getFragmentController().showDialog(dialogFragment, ConfirmationDialogFragment.TAG);
return false;
}
setCurrentDefault(selectedKey);
return true;
}
@Override
protected final String getCurrentCheckedKey() {
return getCurrentDefaultKey();
}
protected TwoStatePreference createOption(DefaultAppInfo info) {
CarUiRadioButtonPreference preference = new CarUiRadioButtonPreference(getContext());
preference.setKey(info.getKey());
preference.setTitle(info.loadLabel());
preference.setIcon(DefaultAppUtils.getSafeIcon(info.loadIcon(),
getContext().getResources().getInteger(R.integer.default_app_safe_icon_size)));
preference.setEnabled(info.enabled);
return preference;
}
/** Gets all of the candidates that should be considered when choosing a default application. */
@NonNull
protected abstract List<DefaultAppInfo> getCandidates();
/** Gets the key of the currently selected candidate. */
protected abstract String getCurrentDefaultKey();
/**
* Sets the key of the currently selected candidate. The implementation of this method should
* modify the value returned by {@link #getCurrentDefaultKey()}}.
*
* @param key represents the key from {@link DefaultAppInfo} which should mark the default
* application.
*/
protected abstract void setCurrentDefault(String key);
/**
* Defines the warning dialog message to be shown when a default app is selected.
*/
protected CharSequence getConfirmationMessage(DefaultAppInfo info) {
return null;
}
/** Gets the current process user id. */
protected int getCurrentProcessUserId() {
return UserHandle.myUserId();
}
/**
* Determines whether the list of default apps should include "none". Implementation classes can
* override this value to {@code false} in order to remove the "none" preference.
*/
protected boolean includeNonePreference() {
return true;
}
private CarUiRadioButtonPreference createNoneOption() {
CarUiRadioButtonPreference preference = new CarUiRadioButtonPreference(getContext());
preference.setKey(NONE_PREFERENCE_KEY);
preference.setTitle(R.string.app_list_preference_none);
preference.setIcon(R.drawable.ic_remove_circle);
return preference;
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.defaultapps;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.Nullable;
import com.android.car.settings.common.FragmentController;
import com.android.car.ui.preference.CarUiTwoActionIconPreference;
import com.android.settingslib.applications.DefaultAppInfo;
/**
* Base preference which handles the logic to display the currently selected default app as well as
* an option to navigate to the settings of the selected default app.
*/
public abstract class DefaultAppsPickerEntryBasePreferenceController extends
DefaultAppEntryBasePreferenceController<CarUiTwoActionIconPreference> {
public DefaultAppsPickerEntryBasePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<CarUiTwoActionIconPreference> getPreferenceType() {
return CarUiTwoActionIconPreference.class;
}
@Override
protected void updateState(CarUiTwoActionIconPreference preference) {
super.updateState(preference);
// If activity does not exist, return. Otherwise allow intenting to the activity.
Intent intent = getSettingIntent(getCurrentDefaultAppInfo());
if (intent == null || intent.resolveActivityInfo(
getContext().getPackageManager(), intent.getFlags()) == null) {
preference.setSecondaryActionVisible(false);
return;
}
// Use startActivityForResult because some apps need to check the identity of the caller.
preference.setOnSecondaryActionClickListener(() -> {
getContext().startActivityForResult(
getContext().getBasePackageName(),
intent,
/* requestCode= */ 0,
/* options= */ null);
});
preference.setSecondaryActionVisible(true);
}
/**
* Returns an optional intent that will be launched when clicking the secondary action icon.
*/
@Nullable
protected Intent getSettingIntent(@Nullable DefaultAppInfo info) {
return null;
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.defaultapps;
import android.app.role.RoleManager;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.service.voice.VoiceInteractionService;
import android.service.voice.VoiceInteractionServiceInfo;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.car.settings.common.FragmentController;
import com.android.car.ui.preference.CarUiTwoActionIconPreference;
import com.android.internal.util.CollectionUtils;
import com.android.settingslib.applications.DefaultAppInfo;
import java.util.List;
/**
* Business logic to show the currently selected default assistant and also show the assistant
* settings, if it exists.
*/
public class DefaultAssistantPickerEntryPreferenceController extends
DefaultAppsPickerEntryBasePreferenceController {
private static final Intent ASSISTANT_SERVICE = new Intent(
VoiceInteractionService.SERVICE_INTERFACE);
private final RoleManager mRoleManager;
public DefaultAssistantPickerEntryPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mRoleManager = getContext().getSystemService(RoleManager.class);
}
@Nullable
@Override
protected DefaultAppInfo getCurrentDefaultAppInfo() {
ComponentName cn = getComponentName();
if (cn == null) {
return null;
}
return new DefaultAppInfo(getContext(), getContext().getPackageManager(),
getCurrentProcessUserId(), cn);
}
@Override
protected boolean handlePreferenceClicked(CarUiTwoActionIconPreference preference) {
String packageName = getContext().getPackageManager().getPermissionControllerPackageName();
if (packageName != null) {
Intent intent = new Intent(Intent.ACTION_MANAGE_DEFAULT_APP)
.setPackage(packageName)
.putExtra(Intent.EXTRA_ROLE_NAME, RoleManager.ROLE_ASSISTANT);
getContext().startActivity(intent);
}
return true;
}
@Nullable
@Override
protected Intent getSettingIntent(@Nullable DefaultAppInfo info) {
ComponentName cn = getComponentName();
if (cn == null) {
return null;
}
return new Intent(Intent.ACTION_MAIN).setComponent(cn);
}
private ComponentName getComponentName() {
String assistantPkgName = CollectionUtils.firstOrNull(
mRoleManager.getRoleHolders(RoleManager.ROLE_ASSISTANT));
if (assistantPkgName == null) {
return null;
}
Intent probe = ASSISTANT_SERVICE.setPackage(assistantPkgName);
PackageManager pm = getContext().getPackageManager();
List<ResolveInfo> services = pm.queryIntentServices(probe, PackageManager.GET_META_DATA);
if (services == null || services.isEmpty()) {
return null;
}
String activity = getAssistSettingsActivity(pm, services.get(0));
if (activity == null) {
return null;
}
return new ComponentName(assistantPkgName, activity);
}
@VisibleForTesting
String getAssistSettingsActivity(PackageManager pm, ResolveInfo resolveInfo) {
VoiceInteractionServiceInfo voiceInfo = new VoiceInteractionServiceInfo(pm,
resolveInfo.serviceInfo);
if (!voiceInfo.getSupportsAssist()) {
return null;
}
return voiceInfo.getSettingsActivity();
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.defaultapps;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.provider.Settings;
import android.service.autofill.AutofillService;
import android.service.autofill.AutofillServiceInfo;
import android.text.TextUtils;
import android.view.autofill.AutofillManager;
import androidx.annotation.Nullable;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.settingslib.applications.DefaultAppInfo;
import java.util.List;
/** Business logic for displaying the currently selected autofill app. */
public class DefaultAutofillPickerEntryPreferenceController extends
DefaultAppsPickerEntryBasePreferenceController {
private static final Logger LOG = new Logger(
DefaultAutofillPickerEntryPreferenceController.class);
private final AutofillManager mAutofillManager;
public DefaultAutofillPickerEntryPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mAutofillManager = context.getSystemService(AutofillManager.class);
}
@Override
protected int getDefaultAvailabilityStatus() {
if (mAutofillManager != null && mAutofillManager.isAutofillSupported()) {
return AVAILABLE;
}
return UNSUPPORTED_ON_DEVICE;
}
@Nullable
@Override
protected DefaultAppInfo getCurrentDefaultAppInfo() {
String flattenComponent = Settings.Secure.getString(getContext().getContentResolver(),
Settings.Secure.AUTOFILL_SERVICE);
if (!TextUtils.isEmpty(flattenComponent)) {
DefaultAppInfo appInfo = new DefaultAppInfo(getContext(),
getContext().getPackageManager(), getCurrentProcessUserId(),
ComponentName.unflattenFromString(flattenComponent));
return appInfo;
}
return null;
}
@Nullable
@Override
protected Intent getSettingIntent(@Nullable DefaultAppInfo info) {
if (info == null) {
return null;
}
Intent intent = new Intent(AutofillService.SERVICE_INTERFACE);
List<ResolveInfo> resolveInfos = getContext().getPackageManager().queryIntentServices(
intent, PackageManager.GET_META_DATA);
for (ResolveInfo resolveInfo : resolveInfos) {
ServiceInfo serviceInfo = resolveInfo.serviceInfo;
String flattenKey = new ComponentName(serviceInfo.packageName,
serviceInfo.name).flattenToString();
if (TextUtils.equals(info.getKey(), flattenKey)) {
String settingsActivity;
try {
settingsActivity = new AutofillServiceInfo(getContext(), serviceInfo)
.getSettingsActivity();
} catch (SecurityException e) {
// Service does not declare the proper permission, ignore it.
LOG.w("Error getting info for " + serviceInfo + ": " + e);
continue;
}
if (TextUtils.isEmpty(settingsActivity)) {
continue;
}
return new Intent(Intent.ACTION_MAIN).setComponent(
new ComponentName(serviceInfo.packageName, settingsActivity));
}
}
return null;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.defaultapps;
import androidx.annotation.XmlRes;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
/** Shows the option to choose the default autofill service. */
public class DefaultAutofillPickerFragment extends SettingsFragment {
@Override
@XmlRes
protected int getPreferenceScreenResId() {
return R.xml.default_autofill_picker_fragment;
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.defaultapps;
import android.Manifest;
import android.car.drivingstate.CarUxRestrictions;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.provider.Settings;
import android.service.autofill.AutofillService;
import android.text.Html;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.settingslib.applications.DefaultAppInfo;
import java.util.ArrayList;
import java.util.List;
/** Business logic for displaying and choosing the default autofill service. */
public class DefaultAutofillPickerPreferenceController extends
DefaultAppsPickerBasePreferenceController {
public DefaultAutofillPickerPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@NonNull
@Override
protected List<DefaultAppInfo> getCandidates() {
List<DefaultAppInfo> candidates = new ArrayList<>();
List<ResolveInfo> resolveInfos = getContext().getPackageManager().queryIntentServices(
new Intent(AutofillService.SERVICE_INTERFACE), PackageManager.GET_META_DATA);
for (ResolveInfo info : resolveInfos) {
String permission = info.serviceInfo.permission;
if (Manifest.permission.BIND_AUTOFILL_SERVICE.equals(permission)) {
candidates.add(new DefaultAppInfo(getContext(), getContext().getPackageManager(),
getCurrentProcessUserId(),
new ComponentName(info.serviceInfo.packageName, info.serviceInfo.name)));
}
}
return candidates;
}
@Override
protected String getCurrentDefaultKey() {
String setting = Settings.Secure.getString(getContext().getContentResolver(),
Settings.Secure.AUTOFILL_SERVICE);
if (setting != null) {
ComponentName componentName = ComponentName.unflattenFromString(setting);
if (componentName != null) {
return componentName.flattenToString();
}
}
return DefaultAppsPickerBasePreferenceController.NONE_PREFERENCE_KEY;
}
@Override
protected void setCurrentDefault(String key) {
Settings.Secure.putString(getContext().getContentResolver(),
Settings.Secure.AUTOFILL_SERVICE, key);
}
@Override
@Nullable
protected CharSequence getConfirmationMessage(DefaultAppInfo info) {
if (info == null) {
return null;
}
CharSequence appName = info.loadLabel();
String message = getContext().getString(R.string.autofill_confirmation_message,
Html.escapeHtml(appName));
return Html.fromHtml(message, Html.FROM_HTML_MODE_LEGACY);
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.managedomainurls;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.UserHandle;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.settingslib.applications.ApplicationsState;
import java.util.List;
/**
* Shared logic for preference controllers related to app launch settings.
*
* @param <V> the upper bound on the type of {@link Preference} on which the controller
* expects to operate.
*/
public abstract class AppLaunchSettingsBasePreferenceController<V extends Preference> extends
PreferenceController<V> {
@VisibleForTesting
static final Intent sBrowserIntent = new Intent()
.setAction(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
.setData(Uri.parse("http:"));
protected final PackageManager mPm;
private ApplicationsState.AppEntry mAppEntry;
public AppLaunchSettingsBasePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
this(context, preferenceKey, fragmentController, uxRestrictions,
context.getPackageManager());
}
@VisibleForTesting
AppLaunchSettingsBasePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions,
PackageManager packageManager) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mPm = packageManager;
}
/** Sets the app entry associated with this settings screen. */
public void setAppEntry(ApplicationsState.AppEntry entry) {
mAppEntry = entry;
}
/** Returns the app entry. */
public ApplicationsState.AppEntry getAppEntry() {
return mAppEntry;
}
/** Returns the package name. */
public String getPackageName() {
return mAppEntry.info.packageName;
}
/** Returns the current user id. */
protected int getCurrentUserId() {
return UserHandle.myUserId();
}
/** Returns {@code true} if the current package is a browser app. */
protected boolean isBrowserApp() {
sBrowserIntent.setPackage(getPackageName());
List<ResolveInfo> list = mPm.queryIntentActivitiesAsUser(sBrowserIntent,
PackageManager.MATCH_ALL, getCurrentUserId());
for (ResolveInfo info : list) {
if (info.activityInfo != null && info.handleAllWebDataURI) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.managedomainurls;
import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS;
import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK;
import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import androidx.annotation.VisibleForTesting;
import androidx.preference.ListPreference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
/**
* Business logic to define how the app should handle related domain links (whether related domain
* links should be opened always, never, or after asking).
*/
public class AppLinkStatePreferenceController extends
AppLaunchSettingsBasePreferenceController<ListPreference> {
private static final Logger LOG = new Logger(AppLinkStatePreferenceController.class);
private boolean mHasDomainUrls;
public AppLinkStatePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@VisibleForTesting
AppLinkStatePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions,
PackageManager packageManager) {
super(context, preferenceKey, fragmentController, uxRestrictions, packageManager);
}
@Override
protected Class<ListPreference> getPreferenceType() {
return ListPreference.class;
}
@Override
protected void onCreateInternal() {
mHasDomainUrls =
(getAppEntry().info.privateFlags & ApplicationInfo.PRIVATE_FLAG_HAS_DOMAIN_URLS)
!= 0;
}
@Override
protected void updateState(ListPreference preference) {
if (isBrowserApp()) {
preference.setEnabled(false);
} else {
preference.setEnabled(mHasDomainUrls);
preference.setEntries(new CharSequence[]{
getContext().getString(R.string.app_link_open_always),
getContext().getString(R.string.app_link_open_ask),
getContext().getString(R.string.app_link_open_never),
});
preference.setEntryValues(new CharSequence[]{
Integer.toString(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS),
Integer.toString(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK),
Integer.toString(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER),
});
if (mHasDomainUrls) {
int state = mPm.getIntentVerificationStatusAsUser(getPackageName(),
getCurrentUserId());
preference.setValueIndex(linkStateToIndex(state));
}
}
}
@Override
protected boolean handlePreferenceChanged(ListPreference preference, Object newValue) {
if (isBrowserApp()) {
// We shouldn't get into this state, but if we do make sure
// not to cause any permanent mayhem.
return false;
}
int newState = Integer.parseInt((String) newValue);
int priorState = mPm.getIntentVerificationStatusAsUser(getPackageName(),
getCurrentUserId());
if (priorState == newState) {
return false;
}
boolean success = mPm.updateIntentVerificationStatusAsUser(getPackageName(), newState,
getCurrentUserId());
if (success) {
// Read back the state to see if the change worked.
int updatedState = mPm.getIntentVerificationStatusAsUser(getPackageName(),
getCurrentUserId());
success = (newState == updatedState);
} else {
LOG.e("Couldn't update intent verification status!");
}
return success;
}
private int linkStateToIndex(int state) {
switch (state) {
case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS:
return getPreference().findIndexOfValue(
Integer.toString(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS));
case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER:
return getPreference().findIndexOfValue(
Integer.toString(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER));
case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK:
default:
return getPreference().findIndexOfValue(
Integer.toString(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK));
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.managedomainurls;
import android.content.Context;
import android.os.Bundle;
import android.os.UserHandle;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.XmlRes;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
import com.android.settingslib.applications.ApplicationsState;
import java.util.Arrays;
import java.util.List;
/** Settings screen to show details about launching a specific app. */
public class ApplicationLaunchSettingsFragment extends SettingsFragment {
@VisibleForTesting
static final String ARG_PACKAGE_NAME = "arg_package_name";
private ApplicationsState mState;
private ApplicationsState.AppEntry mAppEntry;
/** Creates a new instance of this fragment for the package specified in the arguments. */
public static ApplicationLaunchSettingsFragment newInstance(String pkg) {
ApplicationLaunchSettingsFragment fragment = new ApplicationLaunchSettingsFragment();
Bundle args = new Bundle();
args.putString(ARG_PACKAGE_NAME, pkg);
fragment.setArguments(args);
return fragment;
}
@Override
@XmlRes
protected int getPreferenceScreenResId() {
return R.xml.application_launch_settings_fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mState = ApplicationsState.getInstance(requireActivity().getApplication());
String pkgName = getArguments().getString(ARG_PACKAGE_NAME);
mAppEntry = mState.getEntry(pkgName, UserHandle.myUserId());
ApplicationWithVersionPreferenceController appController = use(
ApplicationWithVersionPreferenceController.class,
R.string.pk_opening_links_app_details);
appController.setAppState(mState);
appController.setAppEntry(mAppEntry);
List<AppLaunchSettingsBasePreferenceController> preferenceControllers = Arrays.asList(
use(AppLinkStatePreferenceController.class,
R.string.pk_opening_links_app_details_state),
use(DomainUrlsPreferenceController.class,
R.string.pk_opening_links_app_details_urls),
use(ClearDefaultsPreferenceController.class,
R.string.pk_opening_links_app_details_reset));
for (AppLaunchSettingsBasePreferenceController controller : preferenceControllers) {
controller.setAppEntry(mAppEntry);
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.managedomainurls;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import androidx.preference.Preference;
import com.android.car.settings.applications.ApplicationPreferenceController;
import com.android.car.settings.common.FragmentController;
/** In addition to showing the app name and icon, shows the app version in the summary. */
public class ApplicationWithVersionPreferenceController extends ApplicationPreferenceController {
public ApplicationWithVersionPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected void updateState(Preference preference) {
super.updateState(preference);
preference.setSummary(getAppVersion());
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.managedomainurls;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.usb.IUsbManager;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.settingslib.applications.AppUtils;
/**
* Business logic to reset a user preference for auto launching an app.
*
* <p>i.e. if a user has both NavigationAppA and NavigationAppB installed and NavigationAppA is set
* as the default navigation app, the user can reset that preference to pick a different default
* navigation app.
*/
public class ClearDefaultsPreferenceController extends
AppLaunchSettingsBasePreferenceController<Preference> {
private static final Logger LOG = new Logger(ClearDefaultsPreferenceController.class);
private final IUsbManager mUsbManager;
public ClearDefaultsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
IBinder b = ServiceManager.getService(Context.USB_SERVICE);
mUsbManager = IUsbManager.Stub.asInterface(b);
}
@VisibleForTesting
ClearDefaultsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions,
PackageManager packageManager, IUsbManager iUsbManager) {
super(context, preferenceKey, fragmentController, uxRestrictions, packageManager);
mUsbManager = iUsbManager;
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
@Override
protected void updateState(Preference preference) {
boolean autoLaunchEnabled = AppUtils.hasPreferredActivities(mPm, getPackageName())
|| isDefaultBrowser(getPackageName())
|| hasUsbDefaults(mUsbManager, getPackageName());
preference.setEnabled(autoLaunchEnabled);
if (autoLaunchEnabled) {
preference.setTitle(R.string.auto_launch_reset_text);
preference.setSummary(R.string.auto_launch_enable_text);
} else {
preference.setTitle(R.string.auto_launch_disable_text);
preference.setSummary(null);
}
}
@Override
protected boolean handlePreferenceClicked(Preference preference) {
if (mUsbManager != null) {
int userId = getCurrentUserId();
mPm.clearPackagePreferredActivities(getPackageName());
if (isDefaultBrowser(getPackageName())) {
mPm.setDefaultBrowserPackageNameAsUser(/* packageName= */ null, userId);
}
try {
mUsbManager.clearDefaults(getPackageName(), userId);
} catch (RemoteException e) {
LOG.e("mUsbManager.clearDefaults", e);
}
refreshUi();
}
return true;
}
private boolean isDefaultBrowser(String packageName) {
String defaultBrowser = mPm.getDefaultBrowserPackageNameAsUser(getCurrentUserId());
return packageName.equals(defaultBrowser);
}
private boolean hasUsbDefaults(IUsbManager usbManager, String packageName) {
try {
if (usbManager != null) {
return usbManager.hasDefaults(packageName, getCurrentUserId());
}
} catch (RemoteException e) {
LOG.e("mUsbManager.hasDefaults", e);
}
return false;
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.managedomainurls;
import android.app.Application;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.util.IconDrawableFactory;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.car.ui.preference.CarUiPreference;
import com.android.settingslib.applications.ApplicationsState;
import java.util.ArrayList;
/** Business logic to populate the list of apps that deal with domain urls. */
public class DomainAppPreferenceController extends PreferenceController<PreferenceGroup> {
private final ApplicationsState mApplicationsState;
private final PackageManager mPm;
@VisibleForTesting
final ApplicationsState.Callbacks mApplicationStateCallbacks =
new ApplicationsState.Callbacks() {
@Override
public void onRunningStateChanged(boolean running) {
}
@Override
public void onPackageListChanged() {
}
@Override
public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
rebuildAppList(apps);
}
@Override
public void onPackageIconChanged() {
}
@Override
public void onPackageSizeChanged(String packageName) {
}
@Override
public void onAllSizesComputed() {
}
@Override
public void onLauncherInfoChanged() {
}
@Override
public void onLoadEntriesCompleted() {
mSession.rebuild(ApplicationsState.FILTER_WITH_DOMAIN_URLS,
ApplicationsState.ALPHA_COMPARATOR);
}
};
private ApplicationsState.Session mSession;
public DomainAppPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
this(context, preferenceKey, fragmentController, uxRestrictions,
ApplicationsState.getInstance((Application) context.getApplicationContext()),
context.getPackageManager());
}
@VisibleForTesting
DomainAppPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions,
ApplicationsState applicationsState, PackageManager packageManager) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mApplicationsState = applicationsState;
mPm = packageManager;
}
@Override
protected Class<PreferenceGroup> getPreferenceType() {
return PreferenceGroup.class;
}
@Override
protected void checkInitialized() {
if (mSession == null) {
throw new IllegalStateException("session should be non null by this point");
}
}
/** Sets the lifecycle to create a new session. */
public void setLifecycle(Lifecycle lifecycle) {
mSession = mApplicationsState.newSession(mApplicationStateCallbacks, lifecycle);
}
@Override
protected void onStartInternal() {
// Resume the session earlier than the lifecycle so that cached information is updated
// even if settings is not resumed (for example in multi-display).
mSession.onResume();
}
@Override
protected void onStopInternal() {
// Since we resume early in onStart, make sure we clean up even if we don't receive onPause.
mSession.onPause();
}
private void rebuildAppList(ArrayList<ApplicationsState.AppEntry> apps) {
PreferenceGroup preferenceGroup = getPreference();
preferenceGroup.removeAll();
for (int i = 0; i < apps.size(); i++) {
ApplicationsState.AppEntry entry = apps.get(i);
preferenceGroup.addPreference(createPreference(entry));
}
}
private Preference createPreference(ApplicationsState.AppEntry entry) {
String key = entry.info.packageName + "|" + entry.info.uid;
IconDrawableFactory iconDrawableFactory = IconDrawableFactory.newInstance(getContext());
CarUiPreference preference = new CarUiPreference(getContext());
preference.setKey(key);
preference.setTitle(entry.label);
preference.setSummary(
DomainUrlsUtils.getDomainsSummary(getContext(), entry.info.packageName,
UserHandle.myUserId(),
DomainUrlsUtils.getHandledDomains(mPm, entry.info.packageName)));
preference.setIcon(iconDrawableFactory.getBadgedIcon(entry.info));
preference.setOnPreferenceClickListener(pref -> {
getFragmentController().launchFragment(
ApplicationLaunchSettingsFragment.newInstance(entry.info.packageName));
return true;
});
return preference;
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.managedomainurls;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.util.ArraySet;
import androidx.preference.Preference;
import com.android.car.settings.R;
import com.android.car.settings.common.ConfirmationDialogFragment;
import com.android.car.settings.common.FragmentController;
/** Business logic to generate and see the list of supported domain urls. */
public class DomainUrlsPreferenceController extends
AppLaunchSettingsBasePreferenceController<Preference> {
private ArraySet<String> mDomains;
public DomainUrlsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<Preference> getPreferenceType() {
return Preference.class;
}
@Override
protected void onCreateInternal() {
mDomains = DomainUrlsUtils.getHandledDomains(getContext().getPackageManager(),
getPackageName());
}
@Override
protected void updateState(Preference preference) {
boolean hasDomains = mDomains != null && mDomains.size() > 0;
preference.setEnabled(!isBrowserApp() && hasDomains);
preference.setSummary(
DomainUrlsUtils.getDomainsSummary(getContext(), getPackageName(),
getCurrentUserId(), mDomains));
}
@Override
protected boolean handlePreferenceClicked(Preference preference) {
String newLines = System.lineSeparator() + System.lineSeparator();
String message = String.join(newLines, mDomains);
// Not exactly a "confirmation" dialog, but reusing to remove the need for a custom
// dialog fragment.
ConfirmationDialogFragment dialogFragment = new ConfirmationDialogFragment.Builder(
getContext())
.setTitle(R.string.app_launch_supported_domain_urls_title)
.setMessage(message)
.setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
.build();
getFragmentController().showDialog(dialogFragment, ConfirmationDialogFragment.TAG);
return true;
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.managedomainurls;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.IntentFilterVerificationInfo;
import android.content.pm.PackageManager;
import android.util.ArraySet;
import com.android.car.settings.R;
import java.util.List;
/** Utility functions related to handling application domain urls. */
public final class DomainUrlsUtils {
private DomainUrlsUtils() {
}
/** Get a summary text based on the number of handled domains. */
public static CharSequence getDomainsSummary(Context context, String packageName, int userId,
ArraySet<String> domains) {
PackageManager pm = context.getPackageManager();
// If the user has explicitly said "no" for this package, that's the string we should show.
int domainStatus = pm.getIntentVerificationStatusAsUser(packageName, userId);
if (domainStatus == PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER) {
return context.getText(R.string.domain_urls_summary_none);
}
// Otherwise, ask package manager for the domains for this package, and show the first
// one (or none if there aren't any).
if (domains.isEmpty()) {
return context.getText(R.string.domain_urls_summary_none);
} else if (domains.size() == 1) {
return context.getString(R.string.domain_urls_summary_one, domains.valueAt(0));
} else {
return context.getString(R.string.domain_urls_summary_some, domains.valueAt(0));
}
}
/** Get the list of domains handled by the given package. */
public static ArraySet<String> getHandledDomains(PackageManager pm, String packageName) {
List<IntentFilterVerificationInfo> iviList = pm.getIntentFilterVerifications(packageName);
List<IntentFilter> filters = pm.getAllIntentFilters(packageName);
ArraySet<String> result = new ArraySet<>();
if (iviList != null && iviList.size() > 0) {
for (IntentFilterVerificationInfo ivi : iviList) {
for (String host : ivi.getDomains()) {
result.add(host);
}
}
}
if (filters != null && filters.size() > 0) {
for (IntentFilter filter : filters) {
if (filter.hasCategory(Intent.CATEGORY_BROWSABLE)
&& (filter.hasDataScheme(IntentFilter.SCHEME_HTTP)
|| filter.hasDataScheme(IntentFilter.SCHEME_HTTPS))) {
result.addAll(filter.getHostsList());
}
}
}
return result;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.managedomainurls;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.android.car.settings.common.BaseCarSettingsActivity;
/**
* Starts {@link ManageDomainUrlsFragment} in a separate activity to help with the back navigation
* flow. This setting differs from the other settings in that the user arrives here from the
* PermissionController rather than from within the Settings app itself.
*/
public class ManageDomainUrlsActivity extends BaseCarSettingsActivity {
@Nullable
@Override
protected Fragment getInitialFragment() {
return new ManageDomainUrlsFragment();
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.managedomainurls;
import android.content.Context;
import androidx.annotation.XmlRes;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
/** Fragment which shows a list of applications to manage handled domain urls. */
public class ManageDomainUrlsFragment extends SettingsFragment {
@Override
@XmlRes
protected int getPreferenceScreenResId() {
return R.xml.manage_domain_urls_fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
use(DomainAppPreferenceController.class, R.string.pk_opening_links_options).setLifecycle(
getLifecycle());
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.performance;
import android.content.Context;
import androidx.annotation.NonNull;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Class used to get the disabled packages due to resource overuse.
*/
public class PerfImpactingAppsItemManager {
private Context mContext;
private final List<PerfImpactingAppsListener> mPerfImpactingAppsListeners;
public PerfImpactingAppsItemManager(Context context) {
mContext = context;
mPerfImpactingAppsListeners = new ArrayList<>();
}
/**
* Registers a listener that will be notified once the data is loaded.
*/
public void addListener(@NonNull PerfImpactingAppsListener listener) {
mPerfImpactingAppsListeners.add(listener);
}
/**
* Starts fetching installed apps and counting the non-system apps
*/
public void startLoading() {
ThreadUtils.postOnBackgroundThread(() -> {
int disabledPackagesCount = PerfImpactingAppsUtils.getDisabledPackages(mContext).size();
for (PerfImpactingAppsListener listener : mPerfImpactingAppsListeners) {
ThreadUtils.postOnMainThread(() -> listener
.onPerfImpactingAppsLoaded(disabledPackagesCount));
}
});
}
/**
* Callback that is called once the performance-impacting apps are loaded.
*/
public interface PerfImpactingAppsListener {
/**
* Called when the apps are successfully loaded from secure settings strings and
* PackageManager.
*/
void onPerfImpactingAppsLoaded(int disabledPackagesCount);
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.performance;
import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
import android.car.Car;
import android.car.drivingstate.CarUxRestrictions;
import android.car.watchdog.CarWatchdogManager;
import android.car.watchdog.PackageKillableState;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.util.AttributeSet;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceGroup;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.PreferenceController;
import com.android.car.ui.preference.CarUiTwoActionTextPreference;
import com.android.settingslib.Utils;
import java.io.File;
import java.util.List;
/**
* Displays the list of apps which have been disabled due to resource overuse by CarWatchdogService.
*
* <p>When a user taps the app, the app's detail setting page is shown. On the other hand, if a
* user presses the "Prioritize app" button, they are shown a dialog which allows them to prioritize
* the app, meaning the app can run in the background once again.
*/
public final class PerfImpactingAppsPreferenceController extends
PreferenceController<PreferenceGroup> {
private static final Logger LOG = new Logger(PerfImpactingAppsPreferenceController.class);
@VisibleForTesting
static final String TURN_ON_PRIORITIZE_APP_PERFORMANCE_DIALOG_TAG =
"com.android.car.settings.applications.performance.PrioritizeAppPerformanceDialogTag";
@Nullable
private Car mCar;
@Nullable
private CarWatchdogManager mCarWatchdogManager;
@Nullable
private List<ApplicationInfo> mEntries;
public PerfImpactingAppsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@Override
protected Class<PreferenceGroup> getPreferenceType() {
return PreferenceGroup.class;
}
@Override
protected void onCreateInternal() {
connectToCar();
updateEntries();
}
@Override
protected void onDestroyInternal() {
if (mCar != null) {
mCar.disconnect();
mCar = null;
}
}
@Override
protected void updateState(PreferenceGroup preference) {
if (mEntries == null) {
return;
}
preference.removeAll();
for (int i = 0; i < mEntries.size(); i++) {
ApplicationInfo entry = mEntries.get(i);
PerformanceImpactingAppPreference appPreference =
new PerformanceImpactingAppPreference(getContext(), entry);
setOnPreferenceClickListeners(appPreference, entry);
preference.addPreference(appPreference);
}
}
private void setOnPreferenceClickListeners(PerformanceImpactingAppPreference preference,
ApplicationInfo info) {
String packageName = info.packageName;
UserHandle userHandle = UserHandle.getUserHandleForUid(info.uid);
preference.setOnPreferenceClickListener(p -> {
Intent settingsIntent = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.parse("package:" + packageName));
getContext().startActivity(settingsIntent);
return true;
});
preference.setOnSecondaryActionClickListener(
() -> PerfImpactingAppsUtils.showPrioritizeAppConfirmationDialog(getContext(),
getFragmentController(),
args -> prioritizeApp(packageName, userHandle),
TURN_ON_PRIORITIZE_APP_PERFORMANCE_DIALOG_TAG));
}
private void prioritizeApp(String packageName, UserHandle userHandle) {
if (mCarWatchdogManager == null) {
LOG.e("CarWatchdogManager is null. Could not prioritize '" + packageName + "'.");
connectToCar();
return;
}
int killableState = PerfImpactingAppsUtils.getKillableState(packageName, userHandle,
mCarWatchdogManager);
if (killableState == PackageKillableState.KILLABLE_STATE_NEVER) {
LOG.wtf("Package '" + packageName + "' for user " + userHandle.getIdentifier()
+ " is disabled for resource overuse but has KILLABLE_STATE_NEVER");
// Given wtf might not kill the process, we enable package in order to
// remove from resource overuse disabled package list.
PackageManager packageManager = getContext().getPackageManager();
packageManager.setApplicationEnabledSetting(packageName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, /* flags= */ 0);
new Handler(Looper.getMainLooper()).postDelayed(this::updateEntries,
/* delayMillis= */ 1000);
} else {
mCarWatchdogManager.setKillablePackageAsUser(packageName, userHandle,
/* isKillable= */ false);
}
updateEntries();
}
private void updateEntries() {
mEntries = PerfImpactingAppsUtils.getDisabledAppInfos(getContext());
refreshUi();
}
private void connectToCar() {
if (mCar != null && mCar.isConnected()) {
mCar.disconnect();
mCar = null;
}
mCar = Car.createCar(getContext(), /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
/* statusChangeListener= */ (car, isReady) -> mCarWatchdogManager = isReady
? (CarWatchdogManager) car.getCarManager(Car.CAR_WATCHDOG_SERVICE)
: null);
}
static class PerformanceImpactingAppPreference extends CarUiTwoActionTextPreference {
PerformanceImpactingAppPreference(Context context, ApplicationInfo info) {
super(context);
boolean apkExists = new File(info.sourceDir).exists();
setKey(info.packageName + "|" + info.uid);
setTitle(getLabel(context, info, apkExists));
setIcon(getIconDrawable(context, info, apkExists));
setPersistent(false);
setSecondaryActionText(R.string.performance_impacting_apps_button_label);
}
@Override
protected void init(@Nullable AttributeSet attrs) {
super.init(attrs);
setLayoutResourceInternal(R.layout.car_ui_preference_two_action_text_borderless);
}
private String getLabel(Context context, ApplicationInfo info, boolean apkExists) {
if (!apkExists) {
return info.packageName;
}
CharSequence label = info.loadLabel(context.getPackageManager());
return label != null ? label.toString() : info.packageName;
}
private Drawable getIconDrawable(Context context, ApplicationInfo info,
boolean apkExists) {
if (apkExists) {
return Utils.getBadgedIcon(context, info);
}
return context.getDrawable(
com.android.internal.R.drawable.sym_app_on_sd_unavailable_icon);
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.performance;
import static android.car.settings.CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE;
import android.car.watchdog.CarWatchdogManager;
import android.car.watchdog.PackageKillableState;
import android.content.ContentResolver;
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.Process;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArraySet;
import com.android.car.settings.R;
import com.android.car.settings.common.ConfirmationDialogFragment;
import com.android.car.settings.common.FragmentController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Utility functions for use in Performance-impacting apps settings and Prioritize app settings.
*/
public final class PerfImpactingAppsUtils {
private static final String PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR = ";";
private PerfImpactingAppsUtils() {}
/**
* Returns the {@link android.car.watchdog.PackageKillableState.KillableState} for the package
* and user provided.
*/
public static int getKillableState(String packageName, UserHandle userHandle,
CarWatchdogManager manager) {
return Objects.requireNonNull(manager)
.getPackageKillableStatesAsUser(userHandle).stream()
.filter(pks -> pks.getPackageName().equals(packageName))
.findFirst().map(PackageKillableState::getKillableState).orElse(-1);
}
/**
* Shows confirmation dialog when user chooses to prioritize an app disabled because of resource
* overuse.
*/
public static void showPrioritizeAppConfirmationDialog(Context context,
FragmentController fragmentController,
ConfirmationDialogFragment.ConfirmListener listener, String dialogTag) {
ConfirmationDialogFragment dialogFragment =
new ConfirmationDialogFragment.Builder(context)
.setTitle(R.string.prioritize_app_performance_dialog_title)
.setMessage(R.string.prioritize_app_performance_dialog_text)
.setPositiveButton(R.string.prioritize_app_performance_dialog_action_on,
listener)
.setNegativeButton(R.string.prioritize_app_performance_dialog_action_off,
/* rejectListener= */ null)
.build();
fragmentController.showDialog(dialogFragment, dialogTag);
}
/**
* Returns the set of package names disabled due to resource overuse.
*/
public static Set<String> getDisabledPackages(Context context) {
ContentResolver contentResolverForUser = context.createContextAsUser(
UserHandle.getUserHandleForUid(Process.myUid()), /* flags= */ 0)
.getContentResolver();
return extractPackages(Settings.Secure.getString(contentResolverForUser,
KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE));
}
/**
* Returns a list of application infos disabled due to resource overuse.
*/
public static List<ApplicationInfo> getDisabledAppInfos(Context context) {
Set<String> disabledPackageNames = getDisabledPackages(context);
if (disabledPackageNames.isEmpty()) {
return new ArrayList<>(0);
}
PackageManager packageManager = context.getPackageManager();
List<ResolveInfo> allPackages = packageManager.queryIntentActivities(
new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER),
PackageManager.ResolveInfoFlags.of(PackageManager.GET_RESOLVED_FILTER
| PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS));
List<ApplicationInfo> disabledAppInfos = new ArrayList<>(allPackages.size());
for (int idx = 0; idx < allPackages.size(); idx++) {
ApplicationInfo applicationInfo = allPackages.get(idx).activityInfo.applicationInfo;
if (disabledPackageNames.contains(applicationInfo.packageName)) {
disabledAppInfos.add(applicationInfo);
// Match only the first occurrence of a package.
// |PackageManager#queryIntentActivities| can return duplicate packages.
disabledPackageNames.remove(applicationInfo.packageName);
}
}
return disabledAppInfos;
}
private static ArraySet<String> extractPackages(String settingsString) {
return TextUtils.isEmpty(settingsString) ? new ArraySet<>()
: new ArraySet<>(Arrays.asList(settingsString.split(
PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR)));
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.performance;
import androidx.annotation.XmlRes;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
/**
* Fragment which lists the apps which are impacting the system's performance adversely.
*
* <p>Selecting an application will open the application detail fragment.
*/
public final class PerformanceImpactingAppSettingsFragment extends SettingsFragment {
@Override
@XmlRes
protected int getPreferenceScreenResId() {
return R.xml.performance_impact_apps_fragment;
}
}

View File

@@ -0,0 +1,291 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.specialaccess;
import android.app.Application;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settingslib.applications.ApplicationsState;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Manages a list of {@link ApplicationsState.AppEntry} instances by syncing in the background and
* providing updates via a {@link Callback}. Clients may provide an {@link ExtraInfoBridge} to
* populate the {@link ApplicationsState.AppEntry#extraInfo} field with use case sepecific data.
* Clients may also provide an {@link ApplicationsState.AppFilter} via an {@link AppFilterProvider}
* to determine which entries will appear in the list updates.
*
* <p>Clients should call {@link #init(ExtraInfoBridge, AppFilterProvider, Callback)} to specify
* behavior and then {@link #start()} to begin loading. {@link #stop()} will cancel loading, and
* {@link #destroy()} will clean up resources when this class will no longer be used.
*/
public class AppEntryListManager {
/** Callback for receiving events from {@link AppEntryListManager}. */
public interface Callback {
/**
* Called when the list of {@link ApplicationsState.AppEntry} instances or the {@link
* ApplicationsState.AppEntry#extraInfo} fields have changed.
*/
void onAppEntryListChanged(List<ApplicationsState.AppEntry> entries);
}
/**
* Provides an {@link ApplicationsState.AppFilter} to tailor the entries in the list updates.
*/
public interface AppFilterProvider {
/**
* Returns the filter that should be used to trim the entries list before callback delivery.
*/
ApplicationsState.AppFilter getAppFilter();
}
/** Bridges extra information to {@link ApplicationsState.AppEntry#extraInfo}. */
public interface ExtraInfoBridge {
/**
* Populates the {@link ApplicationsState.AppEntry#extraInfo} field on the {@code enrties}
* with the relevant data for the implementation.
*/
void loadExtraInfo(List<ApplicationsState.AppEntry> entries);
}
@VisibleForTesting
final ApplicationsState.Callbacks mSessionCallbacks =
new ApplicationsState.Callbacks() {
@Override
public void onRunningStateChanged(boolean running) {
// No op.
}
@Override
public void onPackageListChanged() {
forceUpdate();
}
@Override
public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
if (mCallback != null) {
mCallback.onAppEntryListChanged(apps);
}
}
@Override
public void onPackageIconChanged() {
// No op.
}
@Override
public void onPackageSizeChanged(String packageName) {
// No op.
}
@Override
public void onAllSizesComputed() {
// No op.
}
@Override
public void onLauncherInfoChanged() {
// No op.
}
@Override
public void onLoadEntriesCompleted() {
mHasReceivedLoadEntries = true;
forceUpdate();
}
};
private final ApplicationsState mApplicationsState;
private final BackgroundHandler mBackgroundHandler;
private final MainHandler mMainHandler;
private ExtraInfoBridge mExtraInfoBridge;
private AppFilterProvider mFilterProvider;
private Callback mCallback;
private ApplicationsState.Session mSession;
private boolean mHasReceivedLoadEntries;
private boolean mHasReceivedExtraInfo;
public AppEntryListManager(Context context) {
this(context, ApplicationsState.getInstance((Application) context.getApplicationContext()));
}
@VisibleForTesting
AppEntryListManager(Context context, ApplicationsState applicationsState) {
mApplicationsState = applicationsState;
// Run on the same background thread as the ApplicationsState to make sure updates don't
// conflict.
mBackgroundHandler = new BackgroundHandler(new WeakReference<>(this),
mApplicationsState.getBackgroundLooper());
mMainHandler = new MainHandler(new WeakReference<>(this));
}
/**
* Specifies the behavior of this manager.
*
* @param extraInfoBridge an optional bridge to load information into the entries.
* @param filterProvider provides a filter to tailor the contents of the list updates.
* @param callback callback to which updated lists are delivered.
*/
public void init(@Nullable ExtraInfoBridge extraInfoBridge,
@Nullable AppFilterProvider filterProvider,
Callback callback) {
if (mSession != null) {
destroy();
}
mExtraInfoBridge = extraInfoBridge;
mFilterProvider = filterProvider;
mCallback = callback;
mSession = mApplicationsState.newSession(mSessionCallbacks);
}
/**
* Starts loading the information in the background. When loading is finished, the {@link
* Callback} will be notified on the main thread.
*/
public void start() {
mSession.onResume();
}
/**
* Stops any pending loading.
*/
public void stop() {
mSession.onPause();
clearHandlers();
}
/**
* Cleans up internal state when this will no longer be used.
*/
public void destroy() {
mSession.onDestroy();
clearHandlers();
mExtraInfoBridge = null;
mFilterProvider = null;
mCallback = null;
}
/**
* Schedules updates for all {@link ApplicationsState.AppEntry} instances. When loading is
* finished, the {@link Callback} will be notified on the main thread.
*/
public void forceUpdate() {
mBackgroundHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
}
/**
* Schedules an update for the given {@code entry}. When loading is finished, the {@link
* Callback} will be notified on the main thread.
*/
public void forceUpdate(ApplicationsState.AppEntry entry) {
mBackgroundHandler.obtainMessage(BackgroundHandler.MSG_LOAD_PKG,
entry).sendToTarget();
}
private void rebuild() {
if (!mHasReceivedLoadEntries || !mHasReceivedExtraInfo) {
// Don't rebuild the list until all the app entries are loaded.
return;
}
mSession.rebuild((mFilterProvider != null) ? mFilterProvider.getAppFilter()
: ApplicationsState.FILTER_EVERYTHING,
ApplicationsState.ALPHA_COMPARATOR, /* foreground= */ false);
}
private void clearHandlers() {
mBackgroundHandler.removeMessages(BackgroundHandler.MSG_LOAD_ALL);
mBackgroundHandler.removeMessages(BackgroundHandler.MSG_LOAD_PKG);
mMainHandler.removeMessages(MainHandler.MSG_INFO_UPDATED);
}
private void loadInfo(List<ApplicationsState.AppEntry> entries) {
if (mExtraInfoBridge != null) {
mExtraInfoBridge.loadExtraInfo(entries);
}
for (ApplicationsState.AppEntry entry : entries) {
mApplicationsState.ensureIcon(entry);
}
}
private static class BackgroundHandler extends Handler {
private static final int MSG_LOAD_ALL = 1;
private static final int MSG_LOAD_PKG = 2;
private final WeakReference<AppEntryListManager> mOuter;
BackgroundHandler(WeakReference<AppEntryListManager> outer, Looper looper) {
super(looper);
mOuter = outer;
}
@Override
public void handleMessage(Message msg) {
AppEntryListManager outer = mOuter.get();
if (outer == null) {
return;
}
switch (msg.what) {
case MSG_LOAD_ALL:
outer.loadInfo(outer.mSession.getAllApps());
outer.mMainHandler.sendEmptyMessage(MainHandler.MSG_INFO_UPDATED);
break;
case MSG_LOAD_PKG:
ApplicationsState.AppEntry entry = (ApplicationsState.AppEntry) msg.obj;
outer.loadInfo(Collections.singletonList(entry));
outer.mMainHandler.sendEmptyMessage(MainHandler.MSG_INFO_UPDATED);
break;
}
}
}
private static class MainHandler extends Handler {
private static final int MSG_INFO_UPDATED = 1;
private final WeakReference<AppEntryListManager> mOuter;
MainHandler(WeakReference<AppEntryListManager> outer) {
mOuter = outer;
}
@Override
public void handleMessage(Message msg) {
AppEntryListManager outer = mOuter.get();
if (outer == null) {
return;
}
switch (msg.what) {
case MSG_INFO_UPDATED:
outer.mHasReceivedExtraInfo = true;
outer.rebuild();
break;
}
}
}
}

View File

@@ -0,0 +1,236 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.specialaccess;
import android.app.AppOpsManager;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import androidx.annotation.CallSuper;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import com.android.car.settings.R;
import com.android.car.settings.applications.specialaccess.AppStateAppOpsBridge.PermissionState;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.car.ui.preference.CarUiSwitchPreference;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.applications.ApplicationsState.AppFilter;
import com.android.settingslib.applications.ApplicationsState.CompoundFilter;
import java.util.List;
/**
* Displays a list of toggles for applications requesting permission to perform the operation with
* which this controller was initialized. {@link #init(int, String, int)} should be called when
* this controller is instantiated to specify the {@link AppOpsManager} operation code to control
* access for.
*/
public class AppOpsPreferenceController extends PreferenceController<PreferenceGroup> {
private static final AppFilter FILTER_HAS_INFO = new AppFilter() {
@Override
public void init() {
// No op.
}
@Override
public boolean filterApp(AppEntry info) {
return info.extraInfo != null;
}
};
private final AppOpsManager mAppOpsManager;
private final Preference.OnPreferenceChangeListener mOnPreferenceChangeListener =
new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
AppOpPreference appOpPreference = (AppOpPreference) preference;
AppEntry entry = appOpPreference.mEntry;
PermissionState extraInfo = (PermissionState) entry.extraInfo;
boolean allowOp = (Boolean) newValue;
if (allowOp != extraInfo.isPermissible()) {
mAppOpsManager.setMode(mAppOpsOpCode, entry.info.uid,
entry.info.packageName,
allowOp ? AppOpsManager.MODE_ALLOWED : mNegativeOpMode);
// Update the extra info of this entry so that it reflects the new mode.
mAppEntryListManager.forceUpdate(entry);
return true;
}
return false;
}
};
private final AppEntryListManager.Callback mCallback = new AppEntryListManager.Callback() {
@Override
public void onAppEntryListChanged(List<AppEntry> entries) {
mEntries = entries;
refreshUi();
}
};
private int mAppOpsOpCode = AppOpsManager.OP_NONE;
private String mPermission;
private int mNegativeOpMode = -1;
@VisibleForTesting
AppEntryListManager mAppEntryListManager;
private List<AppEntry> mEntries;
private boolean mShowSystem;
public AppOpsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
this(context, preferenceKey, fragmentController, uxRestrictions,
context.getSystemService(AppOpsManager.class));
}
@VisibleForTesting
AppOpsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions,
AppOpsManager appOpsManager) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mAppOpsManager = appOpsManager;
mAppEntryListManager = new AppEntryListManager(context);
}
@Override
protected Class<PreferenceGroup> getPreferenceType() {
return PreferenceGroup.class;
}
/**
* Initializes this controller with the {@code appOpsOpCode} (selected from the operations in
* {@link AppOpsManager}) to control access for.
*
* @param permission the {@link android.Manifest.permission} apps must hold to perform the
* operation.
* @param negativeOpMode the operation mode that will be passed to {@link
* AppOpsManager#setMode(int, int, String, int)} when access for a app is
* revoked.
*/
public void init(int appOpsOpCode, String permission, int negativeOpMode) {
mAppOpsOpCode = appOpsOpCode;
mPermission = permission;
mNegativeOpMode = negativeOpMode;
}
/**
* Rebuilds the preference list to show system applications if {@code showSystem} is true.
* System applications will be hidden otherwise.
*/
public void setShowSystem(boolean showSystem) {
if (mShowSystem != showSystem) {
mShowSystem = showSystem;
mAppEntryListManager.forceUpdate();
}
}
@Override
protected void checkInitialized() {
if (mAppOpsOpCode == AppOpsManager.OP_NONE) {
throw new IllegalStateException("App operation code must be initialized");
}
if (mPermission == null) {
throw new IllegalStateException("Manifest permission must be initialized");
}
if (mNegativeOpMode == -1) {
throw new IllegalStateException("Negative case app operation mode must be initialized");
}
}
@Override
protected void onCreateInternal() {
AppStateAppOpsBridge extraInfoBridge = new AppStateAppOpsBridge(getContext(), mAppOpsOpCode,
mPermission);
mAppEntryListManager.init(extraInfoBridge, this::getAppFilter, mCallback);
}
@Override
protected void onStartInternal() {
mAppEntryListManager.start();
}
@Override
protected void onStopInternal() {
mAppEntryListManager.stop();
}
@Override
protected void onDestroyInternal() {
mAppEntryListManager.destroy();
}
@Override
protected void updateState(PreferenceGroup preference) {
if (mEntries == null) {
// Still loading.
return;
}
preference.removeAll();
for (AppEntry entry : mEntries) {
Preference appOpPreference = new AppOpPreference(getContext(), entry);
appOpPreference.setOnPreferenceChangeListener(mOnPreferenceChangeListener);
preference.addPreference(appOpPreference);
}
}
@CallSuper
protected AppFilter getAppFilter() {
AppFilter filterObj = new CompoundFilter(FILTER_HAS_INFO,
ApplicationsState.FILTER_NOT_HIDE);
if (!mShowSystem) {
filterObj = new CompoundFilter(filterObj,
ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER);
}
return filterObj;
}
private static class AppOpPreference extends CarUiSwitchPreference {
private final AppEntry mEntry;
AppOpPreference(Context context, AppEntry entry) {
super(context);
String key = entry.info.packageName + "|" + entry.info.uid;
setKey(key);
setTitle(entry.label);
setIcon(entry.icon);
setSummary(getAppStateText(entry.info));
setPersistent(false);
PermissionState extraInfo = (PermissionState) entry.extraInfo;
setChecked(extraInfo.isPermissible());
mEntry = entry;
}
private String getAppStateText(ApplicationInfo info) {
if ((info.flags & ApplicationInfo.FLAG_INSTALLED) == 0) {
return getContext().getString(R.string.not_installed);
} else if (!info.enabled || info.enabledSetting
== PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
return getContext().getString(R.string.disabled);
}
return null;
}
}
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.specialaccess;
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArrayMap;
import android.util.SparseArray;
import androidx.annotation.VisibleForTesting;
import com.android.car.settings.common.Logger;
import com.android.internal.util.ArrayUtils;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
import java.util.List;
import java.util.Map;
/**
* Bridges {@link AppOpsManager} app operation permission information into {@link
* AppEntry#extraInfo} as {@link PermissionState} objects.
*/
public class AppStateAppOpsBridge implements AppEntryListManager.ExtraInfoBridge {
private static final Logger LOG = new Logger(AppStateAppOpsBridge.class);
private final Context mContext;
private final IPackageManager mIPackageManager;
private final List<UserHandle> mProfiles;
private final AppOpsManager mAppOpsManager;
private final int mAppOpsOpCode;
private final String mPermission;
/**
* Constructor.
*
* @param appOpsOpCode the {@link AppOpsManager} op code constant to fetch information for.
* @param permission the {@link android.Manifest.permission} required to perform the
* operation.
*/
public AppStateAppOpsBridge(Context context, int appOpsOpCode, String permission) {
this(context, appOpsOpCode, permission, AppGlobals.getPackageManager(),
UserManager.get(context).getUserProfiles(),
context.getSystemService(AppOpsManager.class));
}
@VisibleForTesting
AppStateAppOpsBridge(Context context, int appOpsOpCode, String permission,
IPackageManager packageManager, List<UserHandle> profiles,
AppOpsManager appOpsManager) {
mContext = context;
mIPackageManager = packageManager;
mProfiles = profiles;
mAppOpsManager = appOpsManager;
mAppOpsOpCode = appOpsOpCode;
mPermission = permission;
}
@Override
public void loadExtraInfo(List<AppEntry> entries) {
SparseArray<Map<String, PermissionState>> packageToStatesMapByProfileId =
getPackageToStateMapsByProfileId();
loadAppOpModes(packageToStatesMapByProfileId);
for (AppEntry entry : entries) {
Map<String, PermissionState> packageStatesMap = packageToStatesMapByProfileId.get(
UserHandle.getUserId(entry.info.uid));
entry.extraInfo = (packageStatesMap != null) ? packageStatesMap.get(
entry.info.packageName) : null;
}
}
private SparseArray<Map<String, PermissionState>> getPackageToStateMapsByProfileId() {
SparseArray<Map<String, PermissionState>> entries = new SparseArray<>();
try {
for (UserHandle profile : mProfiles) {
int profileId = profile.getIdentifier();
List<PackageInfo> packageInfos = getPackageInfos(profileId);
Map<String, PermissionState> entriesForProfile = new ArrayMap<>();
entries.put(profileId, entriesForProfile);
for (PackageInfo packageInfo : packageInfos) {
boolean isAvailable = mIPackageManager.isPackageAvailable(
packageInfo.packageName,
profileId);
if (shouldIgnorePackage(packageInfo) || !isAvailable) {
LOG.d("Ignoring " + packageInfo.packageName + " isAvailable="
+ isAvailable);
continue;
}
PermissionState newEntry = new PermissionState();
newEntry.mRequestedPermissions = packageInfo.requestedPermissions;
entriesForProfile.put(packageInfo.packageName, newEntry);
}
}
} catch (RemoteException e) {
LOG.w("PackageManager is dead. Can't get list of packages requesting "
+ mPermission, e);
}
return entries;
}
@SuppressWarnings("unchecked") // safe by specification.
private List<PackageInfo> getPackageInfos(int profileId) throws RemoteException {
return mIPackageManager.getPackagesHoldingPermissions(new String[]{mPermission},
PackageManager.GET_PERMISSIONS, profileId).getList();
}
private boolean shouldIgnorePackage(PackageInfo packageInfo) {
return packageInfo.packageName.equals("android")
|| packageInfo.packageName.equals(mContext.getPackageName())
|| !ArrayUtils.contains(packageInfo.requestedPermissions, mPermission);
}
/** Sets the {@link PermissionState#mAppOpMode} field. */
private void loadAppOpModes(
SparseArray<Map<String, PermissionState>> packageToStateMapsByProfileId) {
// Find out which packages have been granted permission from AppOps.
List<AppOpsManager.PackageOps> packageOps = mAppOpsManager.getPackagesForOps(
new int[]{mAppOpsOpCode});
if (packageOps == null) {
return;
}
for (AppOpsManager.PackageOps packageOp : packageOps) {
int userId = UserHandle.getUserId(packageOp.getUid());
Map<String, PermissionState> packageStateMap = packageToStateMapsByProfileId.get(
userId);
if (packageStateMap == null) {
// Profile is not for the current user.
continue;
}
PermissionState permissionState = packageStateMap.get(packageOp.getPackageName());
if (permissionState == null) {
LOG.w("AppOp permission exists for package " + packageOp.getPackageName()
+ " of user " + userId + " but package doesn't exist or did not request "
+ mPermission + " access");
continue;
}
if (packageOp.getOps().size() < 1) {
LOG.w("No AppOps permission exists for package " + packageOp.getPackageName());
continue;
}
permissionState.mAppOpMode = packageOp.getOps().get(0).getMode();
}
}
/**
* Data class for use in {@link AppEntry#extraInfo} which indicates whether
* the app operation used to construct the data bridge is permitted for the associated
* application.
*/
public static class PermissionState {
private String[] mRequestedPermissions;
private int mAppOpMode = AppOpsManager.MODE_DEFAULT;
/** Returns {@code true} if the entry's application is allowed to perform the operation. */
public boolean isPermissible() {
// Default behavior is permissible as long as the package requested this permission.
if (mAppOpMode == AppOpsManager.MODE_DEFAULT) {
return true;
}
return mAppOpMode == AppOpsManager.MODE_ALLOWED;
}
/** Returns the permissions requested by the entry's application. */
public String[] getRequestedPermissions() {
return mRequestedPermissions;
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.specialaccess;
import android.telephony.SmsManager;
import com.android.settingslib.applications.ApplicationsState;
import java.util.List;
/**
* Bridges the value of {@link SmsManager#getPremiumSmsConsent(String)} into the {@link
* ApplicationsState.AppEntry#extraInfo} for each entry's package name.
*/
public class AppStatePremiumSmsBridge implements AppEntryListManager.ExtraInfoBridge {
private final SmsManager mSmsManager;
public AppStatePremiumSmsBridge(SmsManager smsManager) {
mSmsManager = smsManager;
}
@Override
public void loadExtraInfo(List<ApplicationsState.AppEntry> entries) {
for (ApplicationsState.AppEntry entry : entries) {
entry.extraInfo = getSmsState(entry.info.packageName);
}
}
private int getSmsState(String packageName) {
return mSmsManager.getPremiumSmsConsent(packageName);
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.applications.specialaccess;
import android.Manifest;
import android.app.AppOpsManager;
import android.content.Context;
import androidx.annotation.XmlRes;
import com.android.car.settings.R;
import com.android.car.settings.applications.AppListFragment;
/**
* Displays apps which have requested to modify system settings and their current allowed status.
*/
public class ModifySystemSettingsFragment extends AppListFragment {
@Override
@XmlRes
protected int getPreferenceScreenResId() {
return R.xml.modify_system_settings_fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
use(AppOpsPreferenceController.class, R.string.pk_modify_system_settings).init(
AppOpsManager.OP_WRITE_SETTINGS,
Manifest.permission.WRITE_SETTINGS,
AppOpsManager.MODE_ERRORED);
}
@Override
protected void onToggleShowSystemApps(boolean showSystem) {
use(AppOpsPreferenceController.class, R.string.pk_modify_system_settings).setShowSystem(
showSystem);
}
}

Some files were not shown because too many files have changed in this diff Show More