• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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