blob: 55c582ed47b4bac379090477d829d2aa9e740bcd [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc.
*
* 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.server.policy;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.ActivityManager;
import android.app.ActivityThread;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.media.AudioAttributes;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Handler;
import android.os.UserHandle;
import android.os.Vibrator;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Slog;
import android.view.Window;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
import android.widget.Toast;
import com.android.internal.R;
import java.util.List;
import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
/**
* Class to help manage the accessibility shortcut
*/
public class AccessibilityShortcutController {
private static final String TAG = "AccessibilityShortcutController";
private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
.build();
private final Context mContext;
private AlertDialog mAlertDialog;
private boolean mIsShortcutEnabled;
private boolean mEnabledOnLockScreen;
private int mUserId;
// Visible for testing
public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider();
public static String getTargetServiceComponentNameString(
Context context, int userId) {
final String currentShortcutServiceId = Settings.Secure.getStringForUser(
context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
userId);
if (currentShortcutServiceId != null) {
return currentShortcutServiceId;
}
return context.getString(R.string.config_defaultAccessibilityService);
}
public AccessibilityShortcutController(Context context, Handler handler, int initialUserId) {
mContext = context;
mUserId = initialUserId;
// Keep track of state of shortcut settings
final ContentObserver co = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange, Uri uri, int userId) {
if (userId == mUserId) {
onSettingsChanged();
}
}
};
mContext.getContentResolver().registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE),
false, co, UserHandle.USER_ALL);
mContext.getContentResolver().registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED),
false, co, UserHandle.USER_ALL);
mContext.getContentResolver().registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN),
false, co, UserHandle.USER_ALL);
setCurrentUser(mUserId);
}
public void setCurrentUser(int currentUserId) {
mUserId = currentUserId;
onSettingsChanged();
}
/**
* Check if the shortcut is available.
*
* @param onLockScreen Whether or not the phone is currently locked.
*
* @return {@code true} if the shortcut is available
*/
public boolean isAccessibilityShortcutAvailable(boolean phoneLocked) {
return mIsShortcutEnabled && (!phoneLocked || mEnabledOnLockScreen);
}
public void onSettingsChanged() {
final boolean haveValidService =
!TextUtils.isEmpty(getTargetServiceComponentNameString(mContext, mUserId));
final ContentResolver cr = mContext.getContentResolver();
final boolean enabled = Settings.Secure.getIntForUser(
cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 1, mUserId) == 1;
mEnabledOnLockScreen = Settings.Secure.getIntForUser(
cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN, 0, mUserId) == 1;
mIsShortcutEnabled = enabled && haveValidService;
}
/**
* Called when the accessibility shortcut is activated
*/
public void performAccessibilityShortcut() {
Slog.d(TAG, "Accessibility shortcut activated");
final ContentResolver cr = mContext.getContentResolver();
final int userId = ActivityManager.getCurrentUser();
final int dialogAlreadyShown = Settings.Secure.getIntForUser(
cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
// Use USAGE_ASSISTANCE_ACCESSIBILITY for TVs to ensure that TVs play the ringtone as they
// have less ways of providing feedback like vibration.
final int audioAttributesUsage = hasFeatureLeanback()
? AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY
: AudioAttributes.USAGE_NOTIFICATION_EVENT;
// Play a notification tone
final Ringtone tone =
RingtoneManager.getRingtone(mContext, Settings.System.DEFAULT_NOTIFICATION_URI);
if (tone != null) {
tone.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(audioAttributesUsage)
.build());
tone.play();
}
// Play a notification vibration
Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
if ((vibrator != null) && vibrator.hasVibrator()) {
// Don't check if haptics are disabled, as we need to alert the user that their
// way of interacting with the phone may change if they activate the shortcut
long[] vibePattern = PhoneWindowManager.getLongIntArray(mContext.getResources(),
R.array.config_longPressVibePattern);
vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES);
}
if (dialogAlreadyShown == 0) {
// The first time, we show a warning rather than toggle the service to give the user a
// chance to turn off this feature before stuff gets enabled.
mAlertDialog = createShortcutWarningDialog(userId);
if (mAlertDialog == null) {
return;
}
Window w = mAlertDialog.getWindow();
WindowManager.LayoutParams attr = w.getAttributes();
attr.type = TYPE_KEYGUARD_DIALOG;
w.setAttributes(attr);
mAlertDialog.show();
Settings.Secure.putIntForUser(
cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 1, userId);
} else {
if (mAlertDialog != null) {
mAlertDialog.dismiss();
mAlertDialog = null;
}
// Show a toast alerting the user to what's happening
final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
if (serviceInfo == null) {
Slog.e(TAG, "Accessibility shortcut set to invalid service");
return;
}
String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo)
? R.string.accessibility_shortcut_disabling_service
: R.string.accessibility_shortcut_enabling_service);
String toastMessage = String.format(toastMessageFormatString,
serviceInfo.getResolveInfo()
.loadLabel(mContext.getPackageManager()).toString());
Toast warningToast = mFrameworkObjectProvider.makeToastFromText(
mContext, toastMessage, Toast.LENGTH_LONG);
warningToast.getWindowParams().privateFlags |=
WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
warningToast.show();
mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)
.performAccessibilityShortcut();
}
}
private AlertDialog createShortcutWarningDialog(int userId) {
final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
if (serviceInfo == null) {
return null;
}
final String warningMessage = String.format(
mContext.getString(R.string.accessibility_shortcut_toogle_warning),
serviceInfo.getResolveInfo().loadLabel(mContext.getPackageManager()).toString());
final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder(
// Use SystemUI context so we pick up any theme set in a vendor overlay
ActivityThread.currentActivityThread().getSystemUiContext())
.setTitle(R.string.accessibility_shortcut_warning_dialog_title)
.setMessage(warningMessage)
.setCancelable(false)
.setPositiveButton(R.string.leave_accessibility_shortcut_on, null)
.setNegativeButton(R.string.disable_accessibility_shortcut,
(DialogInterface d, int which) -> {
Settings.Secure.putStringForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
userId);
})
.setOnCancelListener((DialogInterface d) -> {
// If canceled, treat as if the dialog has never been shown
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
})
.create();
return alertDialog;
}
private AccessibilityServiceInfo getInfoForTargetService() {
final String currentShortcutServiceString = getTargetServiceComponentNameString(
mContext, UserHandle.USER_CURRENT);
if (currentShortcutServiceString == null) {
return null;
}
AccessibilityManager accessibilityManager =
mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
return accessibilityManager.getInstalledServiceInfoWithComponentName(
ComponentName.unflattenFromString(currentShortcutServiceString));
}
private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) {
AccessibilityManager accessibilityManager =
mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
return accessibilityManager.getEnabledAccessibilityServiceList(
AccessibilityServiceInfo.FEEDBACK_ALL_MASK).contains(serviceInfo);
}
private boolean hasFeatureLeanback() {
return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
}
// Class to allow mocking of static framework calls
public static class FrameworkObjectProvider {
public AccessibilityManager getAccessibilityManagerInstance(Context context) {
return AccessibilityManager.getInstance(context);
}
public AlertDialog.Builder getAlertDialogBuilder(Context context) {
return new AlertDialog.Builder(context);
}
public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) {
return Toast.makeText(context, charSequence, duration);
}
}
}