feat: SettingsLib验证通过-使用App module启用
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
191
CarSettings/src/com/android/car/settings/FallbackHome.java
Normal file
191
CarSettings/src/com/android/car/settings/FallbackHome.java
Normal 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
12
CarSettings/src/com/android/car/settings/TEST_MAPPING
Normal file
12
CarSettings/src/com/android/car/settings/TEST_MAPPING
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"auto-end-to-end-postsubmit": [
|
||||
{
|
||||
"name": "AndroidAutomotiveSettingsTests",
|
||||
"options" : [
|
||||
{
|
||||
"include-filter": "android.platform.tests.SettingTest"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user