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.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
21 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY;
22 
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.content.om.IOverlayManager;
28 import android.content.om.OverlayInfo;
29 import android.content.pm.PackageManager;
30 import android.content.res.ApkAssets;
31 import android.os.PatternMatcher;
32 import android.os.RemoteException;
33 import android.os.ServiceManager;
34 import android.os.UserHandle;
35 import android.provider.Settings;
36 import android.provider.Settings.Secure;
37 import android.util.Log;
38 
39 import com.android.systemui.Dumpable;
40 import com.android.systemui.dagger.qualifiers.UiBackground;
41 import com.android.systemui.shared.system.ActivityManagerWrapper;
42 import com.android.systemui.statusbar.policy.ConfigurationController;
43 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
44 
45 import java.io.FileDescriptor;
46 import java.io.PrintWriter;
47 import java.util.ArrayList;
48 import java.util.concurrent.Executor;
49 
50 import javax.inject.Inject;
51 import javax.inject.Singleton;
52 
53 /**
54  * Controller for tracking the current navigation bar mode.
55  */
56 @Singleton
57 public class NavigationModeController implements Dumpable {
58 
59     private static final String TAG = NavigationModeController.class.getSimpleName();
60     private static final boolean DEBUG = true;
61 
62     public interface ModeChangedListener {
onNavigationModeChanged(int mode)63         void onNavigationModeChanged(int mode);
64     }
65 
66     private final Context mContext;
67     private Context mCurrentUserContext;
68     private final IOverlayManager mOverlayManager;
69     private final Executor mUiBgExecutor;
70 
71     private ArrayList<ModeChangedListener> mListeners = new ArrayList<>();
72 
73     private final DeviceProvisionedController.DeviceProvisionedListener mDeviceProvisionedCallback =
74             new DeviceProvisionedController.DeviceProvisionedListener() {
75                 @Override
76                 public void onUserSwitched() {
77                     if (DEBUG) {
78                         Log.d(TAG, "onUserSwitched: "
79                                 + ActivityManagerWrapper.getInstance().getCurrentUserId());
80                     }
81 
82                     // Update the nav mode for the current user
83                     updateCurrentInteractionMode(true /* notify */);
84                 }
85             };
86 
87     // The primary user SysUI process doesn't get AppInfo changes from overlay package changes for
88     // the secondary user (b/158613864), so we need to update the interaction mode here as well
89     // as a fallback if we don't receive the configuration change
90     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
91         @Override
92         public void onReceive(Context context, Intent intent) {
93             if (DEBUG) {
94                 Log.d(TAG, "ACTION_OVERLAY_CHANGED");
95             }
96             updateCurrentInteractionMode(true /* notify */);
97         }
98     };
99 
100 
101     @Inject
NavigationModeController(Context context, DeviceProvisionedController deviceProvisionedController, ConfigurationController configurationController, @UiBackground Executor uiBgExecutor)102     public NavigationModeController(Context context,
103             DeviceProvisionedController deviceProvisionedController,
104             ConfigurationController configurationController,
105             @UiBackground Executor uiBgExecutor) {
106         mContext = context;
107         mCurrentUserContext = context;
108         mOverlayManager = IOverlayManager.Stub.asInterface(
109                 ServiceManager.getService(Context.OVERLAY_SERVICE));
110         mUiBgExecutor = uiBgExecutor;
111         deviceProvisionedController.addCallback(mDeviceProvisionedCallback);
112 
113         IntentFilter overlayFilter = new IntentFilter(ACTION_OVERLAY_CHANGED);
114         overlayFilter.addDataScheme("package");
115         overlayFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL);
116         mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, overlayFilter, null, null);
117 
118         configurationController.addCallback(new ConfigurationController.ConfigurationListener() {
119             @Override
120             public void onOverlayChanged() {
121                 if (DEBUG) {
122                     Log.d(TAG, "onOverlayChanged");
123                 }
124                 updateCurrentInteractionMode(true /* notify */);
125             }
126         });
127 
128         updateCurrentInteractionMode(false /* notify */);
129     }
130 
updateCurrentInteractionMode(boolean notify)131     public void updateCurrentInteractionMode(boolean notify) {
132         mCurrentUserContext = getCurrentUserContext();
133         int mode = getCurrentInteractionMode(mCurrentUserContext);
134         if (mode == NAV_BAR_MODE_GESTURAL) {
135             switchToDefaultGestureNavOverlayIfNecessary();
136         }
137         mUiBgExecutor.execute(() ->
138             Settings.Secure.putString(mCurrentUserContext.getContentResolver(),
139                     Secure.NAVIGATION_MODE, String.valueOf(mode)));
140         if (DEBUG) {
141             Log.e(TAG, "updateCurrentInteractionMode: mode=" + mode);
142             dumpAssetPaths(mCurrentUserContext);
143         }
144 
145         if (notify) {
146             for (int i = 0; i < mListeners.size(); i++) {
147                 mListeners.get(i).onNavigationModeChanged(mode);
148             }
149         }
150     }
151 
addListener(ModeChangedListener listener)152     public int addListener(ModeChangedListener listener) {
153         mListeners.add(listener);
154         return getCurrentInteractionMode(mCurrentUserContext);
155     }
156 
removeListener(ModeChangedListener listener)157     public void removeListener(ModeChangedListener listener) {
158         mListeners.remove(listener);
159     }
160 
getCurrentInteractionMode(Context context)161     private int getCurrentInteractionMode(Context context) {
162         int mode = context.getResources().getInteger(
163                 com.android.internal.R.integer.config_navBarInteractionMode);
164         if (DEBUG) {
165             Log.d(TAG, "getCurrentInteractionMode: mode=" + mode
166                     + " contextUser=" + context.getUserId());
167         }
168         return mode;
169     }
170 
getCurrentUserContext()171     public Context getCurrentUserContext() {
172         int userId = ActivityManagerWrapper.getInstance().getCurrentUserId();
173         if (DEBUG) {
174             Log.d(TAG, "getCurrentUserContext: contextUser=" + mContext.getUserId()
175                     + " currentUser=" + userId);
176         }
177         if (mContext.getUserId() == userId) {
178             return mContext;
179         }
180         try {
181             return mContext.createPackageContextAsUser(mContext.getPackageName(),
182                     0 /* flags */, UserHandle.of(userId));
183         } catch (PackageManager.NameNotFoundException e) {
184             // Never happens for the sysui package
185             Log.e(TAG, "Failed to create package context", e);
186             return null;
187         }
188     }
189 
switchToDefaultGestureNavOverlayIfNecessary()190     private void switchToDefaultGestureNavOverlayIfNecessary() {
191         final int userId = mCurrentUserContext.getUserId();
192         try {
193             final IOverlayManager om = IOverlayManager.Stub.asInterface(
194                     ServiceManager.getService(Context.OVERLAY_SERVICE));
195             final OverlayInfo info = om.getOverlayInfo(NAV_BAR_MODE_GESTURAL_OVERLAY, userId);
196             if (info != null && !info.isEnabled()) {
197                 // Enable the default gesture nav overlay, and move the back gesture inset scale to
198                 // Settings.Secure for left and right sensitivity.
199                 final int curInset = mCurrentUserContext.getResources().getDimensionPixelSize(
200                         com.android.internal.R.dimen.config_backGestureInset);
201                 om.setEnabledExclusiveInCategory(NAV_BAR_MODE_GESTURAL_OVERLAY, userId);
202                 final int defInset = mCurrentUserContext.getResources().getDimensionPixelSize(
203                         com.android.internal.R.dimen.config_backGestureInset);
204 
205                 final float scale = defInset == 0 ? 1.0f : ((float) curInset) / defInset;
206                 Settings.Secure.putFloat(mCurrentUserContext.getContentResolver(),
207                         Secure.BACK_GESTURE_INSET_SCALE_LEFT, scale);
208                 Settings.Secure.putFloat(mCurrentUserContext.getContentResolver(),
209                         Secure.BACK_GESTURE_INSET_SCALE_RIGHT, scale);
210                 if (DEBUG) {
211                     Log.v(TAG, "Moved back sensitivity for user " + userId + " to scale " + scale);
212                 }
213             }
214         } catch (SecurityException | IllegalStateException | RemoteException e) {
215             Log.e(TAG, "Failed to switch to default gesture nav overlay for user " + userId);
216         }
217     }
218 
219     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)220     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
221         pw.println("NavigationModeController:");
222         pw.println("  mode=" + getCurrentInteractionMode(mCurrentUserContext));
223         String defaultOverlays = "";
224         try {
225             defaultOverlays = String.join(", ", mOverlayManager.getDefaultOverlayPackages());
226         } catch (RemoteException e) {
227             defaultOverlays = "failed_to_fetch";
228         }
229         pw.println("  defaultOverlays=" + defaultOverlays);
230         dumpAssetPaths(mCurrentUserContext);
231     }
232 
dumpAssetPaths(Context context)233     private void dumpAssetPaths(Context context) {
234         Log.d(TAG, "  contextUser=" + mCurrentUserContext.getUserId());
235         Log.d(TAG, "  assetPaths=");
236         ApkAssets[] assets = context.getResources().getAssets().getApkAssets();
237         for (ApkAssets a : assets) {
238             Log.d(TAG, "    " + a.getAssetPath());
239         }
240     }
241 }
242