1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.statusbar.phone; 18 19 import static android.content.Intent.ACTION_OVERLAY_CHANGED; 20 import static android.content.Intent.ACTION_PREFERRED_ACTIVITY_CHANGED; 21 import static android.os.UserHandle.USER_CURRENT; 22 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; 23 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY; 24 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; 25 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY; 26 27 import android.app.Notification; 28 import android.app.NotificationManager; 29 import android.app.PendingIntent; 30 import android.content.BroadcastReceiver; 31 import android.content.ComponentName; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.IntentFilter; 35 import android.content.om.IOverlayManager; 36 import android.content.pm.ApplicationInfo; 37 import android.content.pm.PackageManager; 38 import android.content.res.ApkAssets; 39 import android.os.PatternMatcher; 40 import android.os.RemoteException; 41 import android.os.ServiceManager; 42 import android.os.UserHandle; 43 import android.provider.Settings; 44 import android.provider.Settings.Secure; 45 import android.text.TextUtils; 46 import android.util.Log; 47 import android.util.SparseBooleanArray; 48 49 import com.android.systemui.Dumpable; 50 import com.android.systemui.R; 51 import com.android.systemui.UiOffloadThread; 52 import com.android.systemui.shared.system.ActivityManagerWrapper; 53 import com.android.systemui.statusbar.policy.DeviceProvisionedController; 54 import com.android.systemui.util.NotificationChannels; 55 56 import java.io.FileDescriptor; 57 import java.io.PrintWriter; 58 import java.util.ArrayList; 59 import java.util.Arrays; 60 61 import javax.inject.Inject; 62 import javax.inject.Singleton; 63 64 /** 65 * Controller for tracking the current navigation bar mode. 66 */ 67 @Singleton 68 public class NavigationModeController implements Dumpable { 69 70 private static final String TAG = NavigationModeController.class.getSimpleName(); 71 private static final boolean DEBUG = false; 72 73 private static final int SYSTEM_APP_MASK = 74 ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP; 75 static final String SHARED_PREFERENCES_NAME = "navigation_mode_controller_preferences"; 76 static final String PREFS_SWITCHED_FROM_GESTURE_NAV_KEY = "switched_from_gesture_nav"; 77 78 public interface ModeChangedListener { onNavigationModeChanged(int mode)79 void onNavigationModeChanged(int mode); 80 } 81 82 private final Context mContext; 83 private Context mCurrentUserContext; 84 private final IOverlayManager mOverlayManager; 85 private final DeviceProvisionedController mDeviceProvisionedController; 86 private final UiOffloadThread mUiOffloadThread; 87 88 private SparseBooleanArray mRestoreGesturalNavBarMode = new SparseBooleanArray(); 89 90 private int mMode = NAV_BAR_MODE_3BUTTON; 91 private ArrayList<ModeChangedListener> mListeners = new ArrayList<>(); 92 93 private String mLastDefaultLauncher; 94 95 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 96 @Override 97 public void onReceive(Context context, Intent intent) { 98 switch (intent.getAction()) { 99 case ACTION_OVERLAY_CHANGED: 100 if (DEBUG) { 101 Log.d(TAG, "ACTION_OVERLAY_CHANGED"); 102 } 103 updateCurrentInteractionMode(true /* notify */); 104 break; 105 case ACTION_PREFERRED_ACTIVITY_CHANGED: 106 if (DEBUG) { 107 Log.d(TAG, "ACTION_PREFERRED_ACTIVITY_CHANGED"); 108 } 109 final String launcher = getDefaultLauncherPackageName(mCurrentUserContext); 110 // Check if it is a default launcher change 111 if (!TextUtils.equals(mLastDefaultLauncher, launcher)) { 112 switchFromGestureNavModeIfNotSupportedByDefaultLauncher(); 113 showNotificationIfDefaultLauncherSupportsGestureNav(); 114 mLastDefaultLauncher = launcher; 115 } 116 break; 117 } 118 } 119 }; 120 121 private final DeviceProvisionedController.DeviceProvisionedListener mDeviceProvisionedCallback = 122 new DeviceProvisionedController.DeviceProvisionedListener() { 123 @Override 124 public void onDeviceProvisionedChanged() { 125 if (DEBUG) { 126 Log.d(TAG, "onDeviceProvisionedChanged: " 127 + mDeviceProvisionedController.isDeviceProvisioned()); 128 } 129 // Once the device has been provisioned, check if we can restore gestural nav 130 restoreGesturalNavOverlayIfNecessary(); 131 } 132 133 @Override 134 public void onUserSetupChanged() { 135 if (DEBUG) { 136 Log.d(TAG, "onUserSetupChanged: " 137 + mDeviceProvisionedController.isCurrentUserSetup()); 138 } 139 // Once the user has been setup, check if we can restore gestural nav 140 restoreGesturalNavOverlayIfNecessary(); 141 } 142 143 @Override 144 public void onUserSwitched() { 145 if (DEBUG) { 146 Log.d(TAG, "onUserSwitched: " 147 + ActivityManagerWrapper.getInstance().getCurrentUserId()); 148 } 149 150 // Update the nav mode for the current user 151 updateCurrentInteractionMode(true /* notify */); 152 switchFromGestureNavModeIfNotSupportedByDefaultLauncher(); 153 154 // When switching users, defer enabling the gestural nav overlay until the user 155 // is all set up 156 deferGesturalNavOverlayIfNecessary(); 157 } 158 }; 159 160 @Inject NavigationModeController(Context context, DeviceProvisionedController deviceProvisionedController, UiOffloadThread uiOffloadThread)161 public NavigationModeController(Context context, 162 DeviceProvisionedController deviceProvisionedController, 163 UiOffloadThread uiOffloadThread) { 164 mContext = context; 165 mCurrentUserContext = context; 166 mOverlayManager = IOverlayManager.Stub.asInterface( 167 ServiceManager.getService(Context.OVERLAY_SERVICE)); 168 mUiOffloadThread = uiOffloadThread; 169 mDeviceProvisionedController = deviceProvisionedController; 170 mDeviceProvisionedController.addCallback(mDeviceProvisionedCallback); 171 172 IntentFilter overlayFilter = new IntentFilter(ACTION_OVERLAY_CHANGED); 173 overlayFilter.addDataScheme("package"); 174 overlayFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL); 175 mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, overlayFilter, null, null); 176 177 IntentFilter preferredActivityFilter = new IntentFilter(ACTION_PREFERRED_ACTIVITY_CHANGED); 178 mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, preferredActivityFilter, null, 179 null); 180 // We are only interested in launcher changes, so keeping track of the current default. 181 mLastDefaultLauncher = getDefaultLauncherPackageName(mContext); 182 183 updateCurrentInteractionMode(false /* notify */); 184 switchFromGestureNavModeIfNotSupportedByDefaultLauncher(); 185 186 // Check if we need to defer enabling gestural nav 187 deferGesturalNavOverlayIfNecessary(); 188 } 189 updateCurrentInteractionMode(boolean notify)190 public void updateCurrentInteractionMode(boolean notify) { 191 mCurrentUserContext = getCurrentUserContext(); 192 int mode = getCurrentInteractionMode(mCurrentUserContext); 193 mMode = mode; 194 mUiOffloadThread.submit(() -> { 195 Settings.Secure.putString(mCurrentUserContext.getContentResolver(), 196 Secure.NAVIGATION_MODE, String.valueOf(mode)); 197 }); 198 if (DEBUG) { 199 Log.e(TAG, "updateCurrentInteractionMode: mode=" + mMode 200 + " contextUser=" + mCurrentUserContext.getUserId()); 201 dumpAssetPaths(mCurrentUserContext); 202 } 203 204 if (notify) { 205 for (int i = 0; i < mListeners.size(); i++) { 206 mListeners.get(i).onNavigationModeChanged(mode); 207 } 208 } 209 } 210 addListener(ModeChangedListener listener)211 public int addListener(ModeChangedListener listener) { 212 mListeners.add(listener); 213 return getCurrentInteractionMode(mCurrentUserContext); 214 } 215 removeListener(ModeChangedListener listener)216 public void removeListener(ModeChangedListener listener) { 217 mListeners.remove(listener); 218 } 219 getCurrentInteractionMode(Context context)220 private int getCurrentInteractionMode(Context context) { 221 int mode = context.getResources().getInteger( 222 com.android.internal.R.integer.config_navBarInteractionMode); 223 if (DEBUG) { 224 Log.d(TAG, "getCurrentInteractionMode: mode=" + mMode 225 + " contextUser=" + context.getUserId()); 226 } 227 return mode; 228 } 229 getCurrentUserContext()230 public Context getCurrentUserContext() { 231 int userId = ActivityManagerWrapper.getInstance().getCurrentUserId(); 232 if (DEBUG) { 233 Log.d(TAG, "getCurrentUserContext: contextUser=" + mContext.getUserId() 234 + " currentUser=" + userId); 235 } 236 if (mContext.getUserId() == userId) { 237 return mContext; 238 } 239 try { 240 return mContext.createPackageContextAsUser(mContext.getPackageName(), 241 0 /* flags */, UserHandle.of(userId)); 242 } catch (PackageManager.NameNotFoundException e) { 243 // Never happens for the sysui package 244 return null; 245 } 246 } 247 deferGesturalNavOverlayIfNecessary()248 private void deferGesturalNavOverlayIfNecessary() { 249 final int userId = mDeviceProvisionedController.getCurrentUser(); 250 mRestoreGesturalNavBarMode.put(userId, false); 251 if (mDeviceProvisionedController.isDeviceProvisioned() 252 && mDeviceProvisionedController.isCurrentUserSetup()) { 253 // User is already setup and device is provisioned, nothing to do 254 if (DEBUG) { 255 Log.d(TAG, "deferGesturalNavOverlayIfNecessary: device is provisioned and user is " 256 + "setup"); 257 } 258 return; 259 } 260 261 ArrayList<String> defaultOverlays = new ArrayList<>(); 262 try { 263 defaultOverlays.addAll(Arrays.asList(mOverlayManager.getDefaultOverlayPackages())); 264 } catch (RemoteException e) { 265 Log.e(TAG, "deferGesturalNavOverlayIfNecessary: failed to fetch default overlays"); 266 } 267 if (!defaultOverlays.contains(NAV_BAR_MODE_GESTURAL_OVERLAY)) { 268 // No default gesture nav overlay 269 if (DEBUG) { 270 Log.d(TAG, "deferGesturalNavOverlayIfNecessary: no default gestural overlay, " 271 + "default=" + defaultOverlays); 272 } 273 return; 274 } 275 276 // If the default is gestural, force-enable three button mode until the device is 277 // provisioned 278 setModeOverlay(NAV_BAR_MODE_3BUTTON_OVERLAY, USER_CURRENT); 279 mRestoreGesturalNavBarMode.put(userId, true); 280 if (DEBUG) { 281 Log.d(TAG, "deferGesturalNavOverlayIfNecessary: setting to 3 button mode"); 282 } 283 } 284 restoreGesturalNavOverlayIfNecessary()285 private void restoreGesturalNavOverlayIfNecessary() { 286 if (DEBUG) { 287 Log.d(TAG, "restoreGesturalNavOverlayIfNecessary: needs restore=" 288 + mRestoreGesturalNavBarMode); 289 } 290 final int userId = mDeviceProvisionedController.getCurrentUser(); 291 if (mRestoreGesturalNavBarMode.get(userId)) { 292 // Restore the gestural state if necessary 293 setModeOverlay(NAV_BAR_MODE_GESTURAL_OVERLAY, USER_CURRENT); 294 mRestoreGesturalNavBarMode.put(userId, false); 295 } 296 } 297 setModeOverlay(String overlayPkg, int userId)298 public void setModeOverlay(String overlayPkg, int userId) { 299 mUiOffloadThread.submit(() -> { 300 try { 301 mOverlayManager.setEnabledExclusiveInCategory(overlayPkg, userId); 302 if (DEBUG) { 303 Log.d(TAG, "setModeOverlay: overlayPackage=" + overlayPkg 304 + " userId=" + userId); 305 } 306 } catch (RemoteException e) { 307 Log.e(TAG, "Failed to enable overlay " + overlayPkg + " for user " + userId); 308 } 309 }); 310 } 311 switchFromGestureNavModeIfNotSupportedByDefaultLauncher()312 private void switchFromGestureNavModeIfNotSupportedByDefaultLauncher() { 313 if (getCurrentInteractionMode(mCurrentUserContext) != NAV_BAR_MODE_GESTURAL) { 314 return; 315 } 316 final Boolean supported = isGestureNavSupportedByDefaultLauncher(mCurrentUserContext); 317 if (supported == null || supported) { 318 return; 319 } 320 321 Log.d(TAG, "Switching system navigation to 3-button mode:" 322 + " defaultLauncher=" + getDefaultLauncherPackageName(mCurrentUserContext) 323 + " contextUser=" + mCurrentUserContext.getUserId()); 324 325 setModeOverlay(NAV_BAR_MODE_3BUTTON_OVERLAY, USER_CURRENT); 326 showNotification(mCurrentUserContext, R.string.notification_content_system_nav_changed); 327 mCurrentUserContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) 328 .edit().putBoolean(PREFS_SWITCHED_FROM_GESTURE_NAV_KEY, true).apply(); 329 } 330 showNotificationIfDefaultLauncherSupportsGestureNav()331 private void showNotificationIfDefaultLauncherSupportsGestureNav() { 332 boolean previouslySwitchedFromGestureNav = mCurrentUserContext 333 .getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) 334 .getBoolean(PREFS_SWITCHED_FROM_GESTURE_NAV_KEY, false); 335 if (!previouslySwitchedFromGestureNav) { 336 return; 337 } 338 if (getCurrentInteractionMode(mCurrentUserContext) == NAV_BAR_MODE_GESTURAL) { 339 return; 340 } 341 final Boolean supported = isGestureNavSupportedByDefaultLauncher(mCurrentUserContext); 342 if (supported == null || !supported) { 343 return; 344 } 345 346 showNotification(mCurrentUserContext, R.string.notification_content_gesture_nav_available); 347 mCurrentUserContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) 348 .edit().putBoolean(PREFS_SWITCHED_FROM_GESTURE_NAV_KEY, false).apply(); 349 } 350 351 /** 352 * Returns null if there is no default launcher set for the current user. Returns true if the 353 * current default launcher supports Gesture Navigation. Returns false otherwise. 354 */ isGestureNavSupportedByDefaultLauncher(Context context)355 private Boolean isGestureNavSupportedByDefaultLauncher(Context context) { 356 final String defaultLauncherPackageName = getDefaultLauncherPackageName(context); 357 if (DEBUG) { 358 Log.d(TAG, "isGestureNavSupportedByDefaultLauncher:" 359 + " defaultLauncher=" + defaultLauncherPackageName 360 + " contextUser=" + context.getUserId()); 361 } 362 if (defaultLauncherPackageName == null) { 363 return null; 364 } 365 if (isSystemApp(context, defaultLauncherPackageName)) { 366 return true; 367 } 368 return false; 369 } 370 getDefaultLauncherPackageName(Context context)371 private String getDefaultLauncherPackageName(Context context) { 372 final ComponentName cn = context.getPackageManager().getHomeActivities(new ArrayList<>()); 373 if (cn == null) { 374 return null; 375 } 376 return cn.getPackageName(); 377 } 378 379 /** Returns true if the app for the given package name is a system app for this device */ isSystemApp(Context context, String packageName)380 private boolean isSystemApp(Context context, String packageName) { 381 try { 382 ApplicationInfo ai = context.getPackageManager().getApplicationInfo(packageName, 383 PackageManager.GET_META_DATA); 384 return ai != null && ((ai.flags & SYSTEM_APP_MASK) != 0); 385 } catch (PackageManager.NameNotFoundException e) { 386 return false; 387 } 388 } 389 showNotification(Context context, int resId)390 private void showNotification(Context context, int resId) { 391 final CharSequence message = context.getResources().getString(resId); 392 if (DEBUG) { 393 Log.d(TAG, "showNotification: message=" + message); 394 } 395 396 final Notification.Builder builder = 397 new Notification.Builder(mContext, NotificationChannels.ALERTS) 398 .setContentText(message) 399 .setStyle(new Notification.BigTextStyle()) 400 .setSmallIcon(R.drawable.ic_info) 401 .setAutoCancel(true) 402 .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(), 0)); 403 context.getSystemService(NotificationManager.class).notify(TAG, 0, builder.build()); 404 } 405 406 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)407 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 408 pw.println("NavigationModeController:"); 409 pw.println(" mode=" + mMode); 410 String defaultOverlays = ""; 411 try { 412 defaultOverlays = String.join(", ", mOverlayManager.getDefaultOverlayPackages()); 413 } catch (RemoteException e) { 414 defaultOverlays = "failed_to_fetch"; 415 } 416 pw.println(" defaultOverlays=" + defaultOverlays); 417 dumpAssetPaths(mCurrentUserContext); 418 419 pw.println(" defaultLauncher=" + mLastDefaultLauncher); 420 boolean previouslySwitchedFromGestureNav = mCurrentUserContext 421 .getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) 422 .getBoolean(PREFS_SWITCHED_FROM_GESTURE_NAV_KEY, false); 423 pw.println(" previouslySwitchedFromGestureNav=" + previouslySwitchedFromGestureNav); 424 } 425 dumpAssetPaths(Context context)426 private void dumpAssetPaths(Context context) { 427 Log.d(TAG, "assetPaths="); 428 ApkAssets[] assets = context.getResources().getAssets().getApkAssets(); 429 for (ApkAssets a : assets) { 430 Log.d(TAG, " " + a.getAssetPath()); 431 } 432 } 433 } 434