1 /* 2 * Copyright (C) 2024 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.ui; 18 19 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_BINDABLE; 20 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON; 21 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW; 22 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI_NEW; 23 24 import android.annotation.Nullable; 25 import android.content.Context; 26 import android.os.Bundle; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.widget.LinearLayout; 30 31 import androidx.annotation.OptIn; 32 import androidx.collection.MutableIntObjectMap; 33 34 import com.android.internal.statusbar.StatusBarIcon; 35 import com.android.internal.statusbar.StatusBarIcon.Shape; 36 import com.android.systemui.Flags; 37 import com.android.systemui.demomode.DemoModeCommandReceiver; 38 import com.android.systemui.kairos.ExperimentalKairosApi; 39 import com.android.systemui.kairos.KairosNetwork; 40 import com.android.systemui.modes.shared.ModesUiIcons; 41 import com.android.systemui.statusbar.BaseStatusBarFrameLayout; 42 import com.android.systemui.statusbar.StatusBarIconView; 43 import com.android.systemui.statusbar.StatusIconDisplayable; 44 import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; 45 import com.android.systemui.statusbar.phone.DemoStatusIcons; 46 import com.android.systemui.statusbar.phone.StatusBarIconHolder; 47 import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder; 48 import com.android.systemui.statusbar.phone.StatusBarLocation; 49 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; 50 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos; 51 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder; 52 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView; 53 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; 54 import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView; 55 import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter; 56 import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView; 57 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel; 58 import com.android.systemui.util.Assert; 59 60 import dagger.Lazy; 61 62 import kotlin.Pair; 63 64 import kotlinx.coroutines.CoroutineScope; 65 import kotlinx.coroutines.Job; 66 67 import java.util.ArrayList; 68 import java.util.HashMap; 69 import java.util.List; 70 import java.util.Map; 71 import java.util.concurrent.CancellationException; 72 73 /** 74 * Turns info from StatusBarIconController into ImageViews in a ViewGroup. 75 */ 76 @OptIn(markerClass = ExperimentalKairosApi.class) 77 public class IconManager implements DemoModeCommandReceiver { 78 protected final ViewGroup mGroup; 79 private final MobileContextProvider mMobileContextProvider; 80 private final LocationBasedWifiViewModel mWifiViewModel; 81 private final MobileIconsViewModel mMobileIconsViewModel; 82 83 private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos; 84 private final KairosNetwork mKairosNetwork; 85 private final CoroutineScope mAppScope; 86 private final MutableIntObjectMap<Job> mBindingJobs = new MutableIntObjectMap<>(); 87 88 /** 89 * Stores the list of bindable icons that have been added, keyed on slot name. This ensures 90 * we don't accidentally add the same bindable icon twice. 91 */ 92 private final Map<String, BindableIconHolder> mBindableIcons = new HashMap<>(); 93 protected final Context mContext; 94 protected int mIconSize; 95 // Whether or not these icons show up in dumpsys 96 protected boolean mShouldLog = false; 97 private StatusBarIconController mController; 98 private final StatusBarLocation mLocation; 99 100 // Enables SystemUI demo mode to take effect in this group 101 protected boolean mDemoable = true; 102 private boolean mIsInDemoMode; 103 protected DemoStatusIcons mDemoStatusIcons; 104 105 protected ArrayList<String> mBlockList = new ArrayList<>(); 106 IconManager( ViewGroup group, StatusBarLocation location, WifiUiAdapter wifiUiAdapter, MobileUiAdapter mobileUiAdapter, Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos, MobileContextProvider mobileContextProvider, KairosNetwork kairosNetwork, CoroutineScope appScope )107 public IconManager( 108 ViewGroup group, 109 StatusBarLocation location, 110 WifiUiAdapter wifiUiAdapter, 111 MobileUiAdapter mobileUiAdapter, 112 Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos, 113 MobileContextProvider mobileContextProvider, 114 KairosNetwork kairosNetwork, 115 CoroutineScope appScope 116 ) { 117 mGroup = group; 118 mMobileContextProvider = mobileContextProvider; 119 mContext = group.getContext(); 120 mLocation = location; 121 mKairosNetwork = kairosNetwork; 122 mAppScope = appScope; 123 124 reloadDimens(); 125 126 // This starts the flow for the new pipeline, and will notify us of changes via 127 // {@link #setNewMobileIconIds} 128 mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel(); 129 MobileIconsBinder.bind(mGroup, mMobileIconsViewModel); 130 131 132 mMobileUiAdapterKairos = mobileUiAdapterKairos; 133 134 mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, mLocation); 135 } 136 isDemoable()137 public boolean isDemoable() { 138 return mDemoable; 139 } 140 setController(StatusBarIconController controller)141 void setController(StatusBarIconController controller) { 142 mController = controller; 143 } 144 145 /** Sets the list of slots that should be blocked from showing in the status bar. */ setBlockList(@ullable List<String> blockList)146 public void setBlockList(@Nullable List<String> blockList) { 147 Assert.isMainThread(); 148 mBlockList.clear(); 149 mBlockList.addAll(blockList); 150 if (mController != null) { 151 mController.refreshIconGroup(this); 152 } 153 } 154 155 /** Sets whether this manager's changes should be dumped in a bug report. */ setShouldLog(boolean should)156 public void setShouldLog(boolean should) { 157 mShouldLog = should; 158 } 159 160 /** Returns true if this manager's changes should be dumped in a bug report. */ shouldLog()161 public boolean shouldLog() { 162 return mShouldLog; 163 } 164 onIconAdded(int index, String slot, boolean blocked, StatusBarIconHolder holder)165 protected void onIconAdded(int index, String slot, boolean blocked, 166 StatusBarIconHolder holder) { 167 addHolder(index, slot, blocked, holder); 168 } 169 addHolder(int index, String slot, boolean blocked, StatusBarIconHolder holder)170 protected StatusIconDisplayable addHolder(int index, String slot, boolean blocked, 171 StatusBarIconHolder holder) { 172 // This is a little hacky, and probably regrettable, but just set `blocked` on any icon 173 // that is in our blocked list, then we'll never see it 174 if (mBlockList.contains(slot)) { 175 blocked = true; 176 } 177 return switch (holder.getType()) { 178 case TYPE_ICON -> addIcon(index, slot, blocked, holder.getIcon()); 179 case TYPE_WIFI_NEW -> addNewWifiIcon(index, slot); 180 case TYPE_MOBILE_NEW -> addNewMobileIcon(index, slot, holder.getTag()); 181 case TYPE_BINDABLE -> 182 // Safe cast, since only BindableIconHolders can set this tag on themselves 183 addBindableIcon((BindableIconHolder) holder, index); 184 default -> null; 185 }; 186 } 187 addIcon(int index, String slot, boolean blocked, StatusBarIcon icon)188 protected StatusBarIconView addIcon(int index, String slot, boolean blocked, 189 StatusBarIcon icon) { 190 StatusBarIconView view = onCreateStatusBarIconView(slot, blocked); 191 view.set(icon); 192 mGroup.addView(view, index, onCreateLayoutParams(icon.shape)); 193 return view; 194 } 195 196 /** 197 * ModernStatusBarViews can be created and bound, and thus do not need to update their 198 * drawable by sending multiple calls to setIcon. Instead, by using a bindable 199 * icon view, we can simply create the icon when requested and allow the 200 * ViewBinder to control its visual state. 201 */ addBindableIcon(BindableIconHolder holder, int index)202 protected StatusIconDisplayable addBindableIcon(BindableIconHolder holder, 203 int index) { 204 mBindableIcons.put(holder.getSlot(), holder); 205 ModernStatusBarView view = holder.getInitializer().createAndBind(mContext); 206 mGroup.addView(view, index, onCreateLayoutParams(Shape.WRAP_CONTENT)); 207 if (mIsInDemoMode) { 208 mDemoStatusIcons.addBindableIcon(holder); 209 } 210 return view; 211 } 212 addNewWifiIcon(int index, String slot)213 protected StatusIconDisplayable addNewWifiIcon(int index, String slot) { 214 ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot); 215 mGroup.addView(view, index, onCreateLayoutParams(Shape.WRAP_CONTENT)); 216 217 if (mIsInDemoMode) { 218 mDemoStatusIcons.addModernWifiView(mWifiViewModel); 219 } 220 221 return view; 222 } 223 224 addNewMobileIcon( int index, String slot, int subId )225 protected StatusIconDisplayable addNewMobileIcon( 226 int index, 227 String slot, 228 int subId 229 ) { 230 BaseStatusBarFrameLayout view = onCreateModernStatusBarMobileView(slot, subId); 231 mGroup.addView(view, index, onCreateLayoutParams(Shape.WRAP_CONTENT)); 232 233 if (mIsInDemoMode) { 234 Context mobileContext = mMobileContextProvider 235 .getMobileContextForSub(subId, mContext); 236 mDemoStatusIcons.addModernMobileView( 237 mobileContext, 238 mMobileIconsViewModel.getLogger(), 239 subId); 240 } 241 242 return view; 243 } 244 onCreateStatusBarIconView(String slot, boolean blocked)245 private StatusBarIconView onCreateStatusBarIconView(String slot, boolean blocked) { 246 return new StatusBarIconView(mContext, slot, null, blocked); 247 } 248 onCreateModernStatusBarWifiView(String slot)249 private ModernStatusBarWifiView onCreateModernStatusBarWifiView(String slot) { 250 return ModernStatusBarWifiView.constructAndBind(mContext, slot, mWifiViewModel); 251 } 252 onCreateModernStatusBarMobileView( String slot, int subId)253 private ModernStatusBarMobileView onCreateModernStatusBarMobileView( 254 String slot, int subId) { 255 Context mobileContext = mMobileContextProvider.getMobileContextForSub(subId, mContext); 256 if (Flags.statusBarMobileIconKairos()) { 257 Pair<ModernStatusBarMobileView, Job> viewAndJob = 258 ModernStatusBarMobileView.constructAndBind( 259 mobileContext, 260 mMobileUiAdapterKairos.get().getMobileIconsViewModel().getLogger(), 261 slot, 262 mMobileUiAdapterKairos.get().getMobileIconsViewModel() 263 .viewModelForSub(subId, mLocation), 264 mAppScope, 265 subId, 266 mLocation, 267 mKairosNetwork 268 ); 269 mBindingJobs.put(subId, viewAndJob.getSecond()); 270 return viewAndJob.getFirst(); 271 } else { 272 return ModernStatusBarMobileView 273 .constructAndBind( 274 mobileContext, 275 mMobileIconsViewModel.getLogger(), 276 slot, 277 mMobileIconsViewModel.viewModelForSub(subId, mLocation) 278 ); 279 } 280 } 281 onCreateLayoutParams(Shape shape)282 protected LinearLayout.LayoutParams onCreateLayoutParams(Shape shape) { 283 int width = ModesUiIcons.isEnabled() && shape == StatusBarIcon.Shape.FIXED_SPACE 284 ? mIconSize 285 : ViewGroup.LayoutParams.WRAP_CONTENT; 286 287 return new LinearLayout.LayoutParams(width, mIconSize); 288 } 289 destroy()290 protected void destroy() { 291 mGroup.removeAllViews(); 292 } 293 reloadDimens()294 protected void reloadDimens() { 295 mIconSize = mContext.getResources().getDimensionPixelSize( 296 com.android.internal.R.dimen.status_bar_icon_size_sp); 297 } 298 onRemoveIcon(int viewIndex)299 protected void onRemoveIcon(int viewIndex) { 300 if (mIsInDemoMode) { 301 mDemoStatusIcons.onRemoveIcon((StatusIconDisplayable) mGroup.getChildAt(viewIndex)); 302 } 303 if (Flags.statusBarMobileIconKairos()) { 304 View view = mGroup.getChildAt(viewIndex); 305 if (view instanceof ModernStatusBarMobileView) { 306 Job bindingJob = mBindingJobs.remove(((ModernStatusBarMobileView) view).getSubId()); 307 if (bindingJob != null) { 308 bindingJob.cancel(new CancellationException()); 309 } 310 } 311 } 312 mGroup.removeViewAt(viewIndex); 313 } 314 315 /** Called once an icon has been set. */ onSetIcon(int viewIndex, StatusBarIcon icon)316 public void onSetIcon(int viewIndex, StatusBarIcon icon) { 317 StatusBarIconView view = (StatusBarIconView) mGroup.getChildAt(viewIndex); 318 if (ModesUiIcons.isEnabled()) { 319 ViewGroup.LayoutParams current = view.getLayoutParams(); 320 ViewGroup.LayoutParams desired = onCreateLayoutParams(icon.shape); 321 if (desired.width != current.width || desired.height != current.height) { 322 view.setLayoutParams(desired); 323 } 324 } 325 view.set(icon); 326 } 327 328 /** Called once an icon holder has been set. */ onSetIconHolder(int viewIndex, StatusBarIconHolder holder)329 public void onSetIconHolder(int viewIndex, StatusBarIconHolder holder) { 330 switch (holder.getType()) { 331 case TYPE_ICON: 332 onSetIcon(viewIndex, holder.getIcon()); 333 return; 334 case TYPE_MOBILE_NEW: 335 case TYPE_WIFI_NEW: 336 case TYPE_BINDABLE: 337 // Nothing, the new icons update themselves 338 return; 339 default: 340 break; 341 } 342 } 343 344 @Override dispatchDemoCommand(String command, Bundle args)345 public void dispatchDemoCommand(String command, Bundle args) { 346 if (!mDemoable) { 347 return; 348 } 349 350 mDemoStatusIcons.dispatchDemoCommand(command, args); 351 } 352 353 @Override onDemoModeStarted()354 public void onDemoModeStarted() { 355 mIsInDemoMode = true; 356 if (mDemoStatusIcons == null) { 357 mDemoStatusIcons = createDemoStatusIcons(); 358 mDemoStatusIcons.addModernWifiView(mWifiViewModel); 359 for (BindableIconHolder holder : mBindableIcons.values()) { 360 mDemoStatusIcons.addBindableIcon(holder); 361 } 362 } 363 mDemoStatusIcons.onDemoModeStarted(); 364 } 365 366 @Override onDemoModeFinished()367 public void onDemoModeFinished() { 368 if (mDemoStatusIcons != null) { 369 mDemoStatusIcons.onDemoModeFinished(); 370 exitDemoMode(); 371 mIsInDemoMode = false; 372 } 373 } 374 exitDemoMode()375 protected void exitDemoMode() { 376 mDemoStatusIcons.remove(); 377 mDemoStatusIcons = null; 378 } 379 createDemoStatusIcons()380 protected DemoStatusIcons createDemoStatusIcons() { 381 return new DemoStatusIcons( 382 (LinearLayout) mGroup, 383 mMobileIconsViewModel, 384 mLocation, 385 mIconSize, 386 mMobileUiAdapterKairos, 387 mKairosNetwork, 388 mAppScope 389 ); 390 } 391 } 392