1 /* 2 * Copyright (C) 2020 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.navigationbar; 18 19 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 20 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; 21 22 import android.annotation.Nullable; 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.graphics.drawable.Icon; 26 import android.util.AttributeSet; 27 import android.util.Log; 28 import android.util.SparseArray; 29 import android.view.Gravity; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.FrameLayout; 34 import android.widget.LinearLayout; 35 import android.widget.Space; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.systemui.Dependency; 39 import com.android.systemui.navigationbar.buttons.ButtonDispatcher; 40 import com.android.systemui.navigationbar.buttons.KeyButtonView; 41 import com.android.systemui.navigationbar.buttons.ReverseLinearLayout; 42 import com.android.systemui.navigationbar.buttons.ReverseLinearLayout.ReverseRelativeLayout; 43 import com.android.systemui.recents.OverviewProxyService; 44 import com.android.systemui.res.R; 45 import com.android.systemui.shared.system.QuickStepContract; 46 47 import java.io.PrintWriter; 48 import java.lang.ref.WeakReference; 49 import java.util.Objects; 50 51 public class NavigationBarInflaterView extends FrameLayout { 52 private static final String TAG = "NavBarInflater"; 53 54 public static final String NAV_BAR_VIEWS = "sysui_nav_bar"; 55 public static final String NAV_BAR_LEFT = "sysui_nav_bar_left"; 56 public static final String NAV_BAR_RIGHT = "sysui_nav_bar_right"; 57 58 public static final String MENU_IME_ROTATE = "menu_ime"; 59 public static final String BACK = "back"; 60 public static final String HOME = "home"; 61 public static final String RECENT = "recent"; 62 public static final String NAVSPACE = "space"; 63 public static final String CLIPBOARD = "clipboard"; 64 public static final String HOME_HANDLE = "home_handle"; 65 public static final String KEY = "key"; 66 public static final String LEFT = "left"; 67 public static final String RIGHT = "right"; 68 public static final String CONTEXTUAL = "contextual"; 69 public static final String IME_SWITCHER = "ime_switcher"; 70 71 public static final String GRAVITY_SEPARATOR = ";"; 72 public static final String BUTTON_SEPARATOR = ","; 73 74 public static final String SIZE_MOD_START = "["; 75 public static final String SIZE_MOD_END = "]"; 76 77 public static final String KEY_CODE_START = "("; 78 public static final String KEY_IMAGE_DELIM = ":"; 79 public static final String KEY_CODE_END = ")"; 80 private static final String WEIGHT_SUFFIX = "W"; 81 private static final String WEIGHT_CENTERED_SUFFIX = "WC"; 82 private static final String ABSOLUTE_SUFFIX = "A"; 83 private static final String ABSOLUTE_VERTICAL_CENTERED_SUFFIX = "C"; 84 85 private static class Listener implements NavigationModeController.ModeChangedListener { 86 private final WeakReference<NavigationBarInflaterView> mSelf; 87 Listener(NavigationBarInflaterView self)88 Listener(NavigationBarInflaterView self) { 89 mSelf = new WeakReference<>(self); 90 } 91 92 @Override onNavigationModeChanged(int mode)93 public void onNavigationModeChanged(int mode) { 94 NavigationBarInflaterView self = mSelf.get(); 95 if (self != null) { 96 self.onNavigationModeChanged(mode); 97 } 98 } 99 } 100 101 private final Listener mListener; 102 103 protected LayoutInflater mLayoutInflater; 104 protected LayoutInflater mLandscapeInflater; 105 106 protected FrameLayout mHorizontal; 107 protected FrameLayout mVertical; 108 109 @VisibleForTesting 110 SparseArray<ButtonDispatcher> mButtonDispatchers; 111 private String mCurrentLayout; 112 113 private View mLastPortrait; 114 private View mLastLandscape; 115 116 private boolean mIsVertical; 117 private boolean mAlternativeOrder; 118 119 private OverviewProxyService mOverviewProxyService; 120 private int mNavBarMode = NAV_BAR_MODE_3BUTTON; 121 NavigationBarInflaterView(Context context, AttributeSet attrs)122 public NavigationBarInflaterView(Context context, AttributeSet attrs) { 123 super(context, attrs); 124 createInflaters(); 125 mOverviewProxyService = Dependency.get(OverviewProxyService.class); 126 mListener = new Listener(this); 127 mNavBarMode = Dependency.get(NavigationModeController.class).addListener(mListener); 128 } 129 130 @VisibleForTesting createInflaters()131 void createInflaters() { 132 mLayoutInflater = LayoutInflater.from(mContext); 133 Configuration landscape = new Configuration(); 134 landscape.setTo(mContext.getResources().getConfiguration()); 135 landscape.orientation = Configuration.ORIENTATION_LANDSCAPE; 136 mLandscapeInflater = LayoutInflater.from(mContext.createConfigurationContext(landscape)); 137 } 138 139 @Override onFinishInflate()140 protected void onFinishInflate() { 141 super.onFinishInflate(); 142 inflateChildren(); 143 clearViews(); 144 inflateLayout(getDefaultLayout()); 145 } 146 inflateChildren()147 private void inflateChildren() { 148 removeAllViews(); 149 mHorizontal = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout, 150 this /* root */, false /* attachToRoot */); 151 addView(mHorizontal); 152 mVertical = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout_vertical, 153 this /* root */, false /* attachToRoot */); 154 addView(mVertical); 155 updateAlternativeOrder(); 156 } 157 getDefaultLayout()158 protected String getDefaultLayout() { 159 final int defaultResource = QuickStepContract.isGesturalMode(mNavBarMode) 160 ? R.string.config_navBarLayoutHandle 161 : mOverviewProxyService.shouldShowSwipeUpUI() 162 ? R.string.config_navBarLayoutQuickstep 163 : R.string.config_navBarLayout; 164 return getContext().getString(defaultResource); 165 } 166 onNavigationModeChanged(int mode)167 private void onNavigationModeChanged(int mode) { 168 mNavBarMode = mode; 169 } 170 171 @Override onDetachedFromWindow()172 protected void onDetachedFromWindow() { 173 Dependency.get(NavigationModeController.class).removeListener(mListener); 174 super.onDetachedFromWindow(); 175 } 176 onLikelyDefaultLayoutChange()177 public void onLikelyDefaultLayoutChange() { 178 // Reevaluate new layout 179 final String newValue = getDefaultLayout(); 180 if (!Objects.equals(mCurrentLayout, newValue)) { 181 clearViews(); 182 inflateLayout(newValue); 183 } 184 } 185 setButtonDispatchers(SparseArray<ButtonDispatcher> buttonDispatchers)186 public void setButtonDispatchers(SparseArray<ButtonDispatcher> buttonDispatchers) { 187 mButtonDispatchers = buttonDispatchers; 188 clearDispatcherViews(); 189 for (int i = 0; i < buttonDispatchers.size(); i++) { 190 initiallyFill(buttonDispatchers.valueAt(i)); 191 } 192 } 193 updateButtonDispatchersCurrentView()194 void updateButtonDispatchersCurrentView() { 195 if (mButtonDispatchers != null) { 196 View view = mIsVertical ? mVertical : mHorizontal; 197 for (int i = 0; i < mButtonDispatchers.size(); i++) { 198 final ButtonDispatcher dispatcher = mButtonDispatchers.valueAt(i); 199 dispatcher.setCurrentView(view); 200 } 201 } 202 } 203 setVertical(boolean vertical)204 void setVertical(boolean vertical) { 205 if (vertical != mIsVertical) { 206 mIsVertical = vertical; 207 } 208 } 209 setAlternativeOrder(boolean alternativeOrder)210 void setAlternativeOrder(boolean alternativeOrder) { 211 if (alternativeOrder != mAlternativeOrder) { 212 mAlternativeOrder = alternativeOrder; 213 updateAlternativeOrder(); 214 } 215 } 216 updateAlternativeOrder()217 private void updateAlternativeOrder() { 218 updateAlternativeOrder(mHorizontal.findViewById(R.id.ends_group)); 219 updateAlternativeOrder(mHorizontal.findViewById(R.id.center_group)); 220 updateAlternativeOrder(mVertical.findViewById(R.id.ends_group)); 221 updateAlternativeOrder(mVertical.findViewById(R.id.center_group)); 222 } 223 updateAlternativeOrder(View v)224 private void updateAlternativeOrder(View v) { 225 if (v instanceof ReverseLinearLayout) { 226 ((ReverseLinearLayout) v).setAlternativeOrder(mAlternativeOrder); 227 } 228 } 229 initiallyFill(ButtonDispatcher buttonDispatcher)230 private void initiallyFill(ButtonDispatcher buttonDispatcher) { 231 addAll(buttonDispatcher, mHorizontal.findViewById(R.id.ends_group)); 232 addAll(buttonDispatcher, mHorizontal.findViewById(R.id.center_group)); 233 addAll(buttonDispatcher, mVertical.findViewById(R.id.ends_group)); 234 addAll(buttonDispatcher, mVertical.findViewById(R.id.center_group)); 235 } 236 addAll(ButtonDispatcher buttonDispatcher, ViewGroup parent)237 private void addAll(ButtonDispatcher buttonDispatcher, ViewGroup parent) { 238 for (int i = 0; i < parent.getChildCount(); i++) { 239 // Need to manually search for each id, just in case each group has more than one 240 // of a single id. It probably mostly a waste of time, but shouldn't take long 241 // and will only happen once. 242 if (parent.getChildAt(i).getId() == buttonDispatcher.getId()) { 243 buttonDispatcher.addView(parent.getChildAt(i)); 244 } 245 if (parent.getChildAt(i) instanceof ViewGroup) { 246 addAll(buttonDispatcher, (ViewGroup) parent.getChildAt(i)); 247 } 248 } 249 } 250 inflateLayout(String newLayout)251 protected void inflateLayout(String newLayout) { 252 mCurrentLayout = newLayout; 253 if (newLayout == null) { 254 newLayout = getDefaultLayout(); 255 } 256 String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3); 257 if (sets.length != 3) { 258 Log.d(TAG, "Invalid layout."); 259 newLayout = getDefaultLayout(); 260 sets = newLayout.split(GRAVITY_SEPARATOR, 3); 261 } 262 String[] start = sets[0].split(BUTTON_SEPARATOR); 263 String[] center = sets[1].split(BUTTON_SEPARATOR); 264 String[] end = sets[2].split(BUTTON_SEPARATOR); 265 // Inflate these in start to end order or accessibility traversal will be messed up. 266 inflateButtons(start, mHorizontal.findViewById(R.id.ends_group), 267 false /* landscape */, true /* start */); 268 inflateButtons(start, mVertical.findViewById(R.id.ends_group), 269 true /* landscape */, true /* start */); 270 271 inflateButtons(center, mHorizontal.findViewById(R.id.center_group), 272 false /* landscape */, false /* start */); 273 inflateButtons(center, mVertical.findViewById(R.id.center_group), 274 true /* landscape */, false /* start */); 275 276 addGravitySpacer(mHorizontal.findViewById(R.id.ends_group)); 277 addGravitySpacer(mVertical.findViewById(R.id.ends_group)); 278 279 inflateButtons(end, mHorizontal.findViewById(R.id.ends_group), 280 false /* landscape */, false /* start */); 281 inflateButtons(end, mVertical.findViewById(R.id.ends_group), 282 true /* landscape */, false /* start */); 283 284 updateButtonDispatchersCurrentView(); 285 } 286 addGravitySpacer(LinearLayout layout)287 private void addGravitySpacer(LinearLayout layout) { 288 layout.addView(new Space(mContext), new LinearLayout.LayoutParams(0, 0, 1)); 289 } 290 inflateButtons(String[] buttons, ViewGroup parent, boolean landscape, boolean start)291 private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape, 292 boolean start) { 293 for (int i = 0; i < buttons.length; i++) { 294 inflateButton(buttons[i], parent, landscape, start); 295 } 296 } 297 copy(ViewGroup.LayoutParams layoutParams)298 private ViewGroup.LayoutParams copy(ViewGroup.LayoutParams layoutParams) { 299 if (layoutParams instanceof LinearLayout.LayoutParams) { 300 return new LinearLayout.LayoutParams(layoutParams.width, layoutParams.height, 301 ((LinearLayout.LayoutParams) layoutParams).weight); 302 } 303 return new LayoutParams(layoutParams.width, layoutParams.height); 304 } 305 306 @Nullable inflateButton(String buttonSpec, ViewGroup parent, boolean landscape, boolean start)307 protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape, 308 boolean start) { 309 LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater; 310 View v = createView(buttonSpec, parent, inflater); 311 if (v == null) return null; 312 313 v = applySize(v, buttonSpec, landscape, start); 314 parent.addView(v); 315 addToDispatchers(v); 316 View lastView = landscape ? mLastLandscape : mLastPortrait; 317 View accessibilityView = v; 318 if (v instanceof ReverseRelativeLayout) { 319 accessibilityView = ((ReverseRelativeLayout) v).getChildAt(0); 320 } 321 if (lastView != null) { 322 accessibilityView.setAccessibilityTraversalAfter(lastView.getId()); 323 } 324 if (landscape) { 325 mLastLandscape = accessibilityView; 326 } else { 327 mLastPortrait = accessibilityView; 328 } 329 return v; 330 } 331 applySize(View v, String buttonSpec, boolean landscape, boolean start)332 private View applySize(View v, String buttonSpec, boolean landscape, boolean start) { 333 String sizeStr = extractSize(buttonSpec); 334 if (sizeStr == null) return v; 335 336 if (sizeStr.contains(WEIGHT_SUFFIX) || sizeStr.contains(ABSOLUTE_SUFFIX)) { 337 // To support gravity, wrap in RelativeLayout and apply gravity to it. 338 // Children wanting to use gravity must be smaller then the frame. 339 ReverseRelativeLayout frame = new ReverseRelativeLayout(mContext); 340 LayoutParams childParams = new LayoutParams(v.getLayoutParams()); 341 342 // Compute gravity to apply 343 int gravity = (landscape) ? (start ? Gravity.TOP : Gravity.BOTTOM) 344 : (start ? Gravity.START : Gravity.END); 345 if (sizeStr.endsWith(WEIGHT_CENTERED_SUFFIX)) { 346 gravity = Gravity.CENTER; 347 } else if (sizeStr.endsWith(ABSOLUTE_VERTICAL_CENTERED_SUFFIX)) { 348 gravity = Gravity.CENTER_VERTICAL; 349 } 350 351 // Set default gravity, flipped if needed in reversed layouts (270 RTL and 90 LTR) 352 frame.setDefaultGravity(gravity); 353 frame.setGravity(gravity); // Apply gravity to root 354 355 frame.addView(v, childParams); 356 357 if (sizeStr.contains(WEIGHT_SUFFIX)) { 358 // Use weighting to set the width of the frame 359 float weight = Float.parseFloat( 360 sizeStr.substring(0, sizeStr.indexOf(WEIGHT_SUFFIX))); 361 frame.setLayoutParams(new LinearLayout.LayoutParams(0, MATCH_PARENT, weight)); 362 } else { 363 int width = (int) convertDpToPx(mContext, 364 Float.parseFloat(sizeStr.substring(0, sizeStr.indexOf(ABSOLUTE_SUFFIX)))); 365 frame.setLayoutParams(new LinearLayout.LayoutParams(width, MATCH_PARENT)); 366 } 367 368 // Ensure ripples can be drawn outside bounds 369 frame.setClipChildren(false); 370 frame.setClipToPadding(false); 371 372 return frame; 373 } 374 375 float size = Float.parseFloat(sizeStr); 376 ViewGroup.LayoutParams params = v.getLayoutParams(); 377 params.width = (int) (params.width * size); 378 return v; 379 } 380 createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater)381 View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) { 382 View v = null; 383 String button = extractButton(buttonSpec); 384 if (LEFT.equals(button)) { 385 button = extractButton(NAVSPACE); 386 } else if (RIGHT.equals(button)) { 387 button = extractButton(MENU_IME_ROTATE); 388 } 389 if (HOME.equals(button)) { 390 v = inflater.inflate(R.layout.home, parent, false); 391 } else if (BACK.equals(button)) { 392 v = inflater.inflate(R.layout.back, parent, false); 393 } else if (RECENT.equals(button)) { 394 v = inflater.inflate(R.layout.recent_apps, parent, false); 395 } else if (MENU_IME_ROTATE.equals(button)) { 396 v = inflater.inflate(R.layout.menu_ime, parent, false); 397 } else if (NAVSPACE.equals(button)) { 398 v = inflater.inflate(R.layout.nav_key_space, parent, false); 399 } else if (CLIPBOARD.equals(button)) { 400 v = inflater.inflate(R.layout.clipboard, parent, false); 401 } else if (CONTEXTUAL.equals(button)) { 402 v = inflater.inflate(R.layout.contextual, parent, false); 403 } else if (HOME_HANDLE.equals(button)) { 404 v = inflater.inflate(R.layout.home_handle, parent, false); 405 } else if (IME_SWITCHER.equals(button)) { 406 v = inflater.inflate(R.layout.ime_switcher, parent, false); 407 } else if (button.startsWith(KEY)) { 408 String uri = extractImage(button); 409 int code = extractKeycode(button); 410 v = inflater.inflate(R.layout.custom_key, parent, false); 411 ((KeyButtonView) v).setCode(code); 412 if (uri != null) { 413 if (uri.contains(":")) { 414 ((KeyButtonView) v).loadAsync(Icon.createWithContentUri(uri)); 415 } else if (uri.contains("/")) { 416 int index = uri.indexOf('/'); 417 String pkg = uri.substring(0, index); 418 int id = Integer.parseInt(uri.substring(index + 1)); 419 ((KeyButtonView) v).loadAsync(Icon.createWithResource(pkg, id)); 420 } 421 } 422 } 423 return v; 424 } 425 extractImage(String buttonSpec)426 public static String extractImage(String buttonSpec) { 427 if (!buttonSpec.contains(KEY_IMAGE_DELIM)) { 428 return null; 429 } 430 final int start = buttonSpec.indexOf(KEY_IMAGE_DELIM); 431 String subStr = buttonSpec.substring(start + 1, buttonSpec.indexOf(KEY_CODE_END)); 432 return subStr; 433 } 434 extractKeycode(String buttonSpec)435 public static int extractKeycode(String buttonSpec) { 436 if (!buttonSpec.contains(KEY_CODE_START)) { 437 return 1; 438 } 439 final int start = buttonSpec.indexOf(KEY_CODE_START); 440 String subStr = buttonSpec.substring(start + 1, buttonSpec.indexOf(KEY_IMAGE_DELIM)); 441 return Integer.parseInt(subStr); 442 } 443 extractSize(String buttonSpec)444 public static String extractSize(String buttonSpec) { 445 if (!buttonSpec.contains(SIZE_MOD_START)) { 446 return null; 447 } 448 final int sizeStart = buttonSpec.indexOf(SIZE_MOD_START); 449 return buttonSpec.substring(sizeStart + 1, buttonSpec.indexOf(SIZE_MOD_END)); 450 } 451 extractButton(String buttonSpec)452 public static String extractButton(String buttonSpec) { 453 if (!buttonSpec.contains(SIZE_MOD_START)) { 454 return buttonSpec; 455 } 456 return buttonSpec.substring(0, buttonSpec.indexOf(SIZE_MOD_START)); 457 } 458 addToDispatchers(View v)459 private void addToDispatchers(View v) { 460 if (mButtonDispatchers != null) { 461 final int indexOfKey = mButtonDispatchers.indexOfKey(v.getId()); 462 if (indexOfKey >= 0) { 463 mButtonDispatchers.valueAt(indexOfKey).addView(v); 464 } 465 if (v instanceof ViewGroup) { 466 final ViewGroup viewGroup = (ViewGroup)v; 467 final int N = viewGroup.getChildCount(); 468 for (int i = 0; i < N; i++) { 469 addToDispatchers(viewGroup.getChildAt(i)); 470 } 471 } 472 } 473 } 474 clearDispatcherViews()475 private void clearDispatcherViews() { 476 if (mButtonDispatchers != null) { 477 for (int i = 0; i < mButtonDispatchers.size(); i++) { 478 mButtonDispatchers.valueAt(i).clear(); 479 } 480 } 481 } 482 clearViews()483 private void clearViews() { 484 clearDispatcherViews(); 485 clearAllChildren(mHorizontal.findViewById(R.id.nav_buttons)); 486 clearAllChildren(mVertical.findViewById(R.id.nav_buttons)); 487 } 488 clearAllChildren(ViewGroup group)489 private void clearAllChildren(ViewGroup group) { 490 for (int i = 0; i < group.getChildCount(); i++) { 491 ((ViewGroup) group.getChildAt(i)).removeAllViews(); 492 } 493 } 494 convertDpToPx(Context context, float dp)495 private static float convertDpToPx(Context context, float dp) { 496 return dp * context.getResources().getDisplayMetrics().density; 497 } 498 dump(PrintWriter pw)499 public void dump(PrintWriter pw) { 500 pw.println("NavigationBarInflaterView"); 501 pw.println(" mCurrentLayout: " + mCurrentLayout); 502 } 503 } 504