1 /*
2  * Copyright (C) 2011 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;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.graphics.Color;
22 import android.graphics.PorterDuff;
23 import android.graphics.drawable.Animatable;
24 import android.graphics.drawable.Drawable;
25 import android.telephony.SubscriptionInfo;
26 import android.util.ArraySet;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.accessibility.AccessibilityEvent;
33 import android.widget.ImageView;
34 import android.widget.LinearLayout;
35 
36 import com.android.systemui.R;
37 import com.android.systemui.statusbar.phone.StatusBarIconController;
38 import com.android.systemui.statusbar.policy.NetworkController.IconState;
39 import com.android.systemui.statusbar.policy.NetworkControllerImpl;
40 import com.android.systemui.statusbar.policy.SecurityController;
41 import com.android.systemui.tuner.TunerService;
42 import com.android.systemui.tuner.TunerService.Tunable;
43 
44 import java.util.ArrayList;
45 import java.util.List;
46 
47 // Intimately tied to the design of res/layout/signal_cluster_view.xml
48 public class SignalClusterView
49         extends LinearLayout
50         implements NetworkControllerImpl.SignalCallback,
51         SecurityController.SecurityControllerCallback, Tunable {
52 
53     static final String TAG = "SignalClusterView";
54     static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
55 
56     private static final String SLOT_AIRPLANE = "airplane";
57     private static final String SLOT_MOBILE = "mobile";
58     private static final String SLOT_WIFI = "wifi";
59     private static final String SLOT_ETHERNET = "ethernet";
60 
61     NetworkControllerImpl mNC;
62     SecurityController mSC;
63 
64     private boolean mNoSimsVisible = false;
65     private boolean mVpnVisible = false;
66     private boolean mEthernetVisible = false;
67     private int mEthernetIconId = 0;
68     private int mLastEthernetIconId = -1;
69     private boolean mWifiVisible = false;
70     private int mWifiStrengthId = 0;
71     private int mLastWifiStrengthId = -1;
72     private boolean mIsAirplaneMode = false;
73     private int mAirplaneIconId = 0;
74     private int mLastAirplaneIconId = -1;
75     private String mAirplaneContentDescription;
76     private String mWifiDescription;
77     private String mEthernetDescription;
78     private ArrayList<PhoneState> mPhoneStates = new ArrayList<PhoneState>();
79     private int mIconTint = Color.WHITE;
80     private float mDarkIntensity;
81 
82     ViewGroup mEthernetGroup, mWifiGroup;
83     View mNoSimsCombo;
84     ImageView mVpn, mEthernet, mWifi, mAirplane, mNoSims, mEthernetDark, mWifiDark, mNoSimsDark;
85     View mWifiAirplaneSpacer;
86     View mWifiSignalSpacer;
87     LinearLayout mMobileSignalGroup;
88 
89     private int mWideTypeIconStartPadding;
90     private int mSecondaryTelephonyPadding;
91     private int mEndPadding;
92     private int mEndPaddingNothingVisible;
93 
94     private boolean mBlockAirplane;
95     private boolean mBlockMobile;
96     private boolean mBlockWifi;
97     private boolean mBlockEthernet;
98 
SignalClusterView(Context context)99     public SignalClusterView(Context context) {
100         this(context, null);
101     }
102 
SignalClusterView(Context context, AttributeSet attrs)103     public SignalClusterView(Context context, AttributeSet attrs) {
104         this(context, attrs, 0);
105     }
106 
SignalClusterView(Context context, AttributeSet attrs, int defStyle)107     public SignalClusterView(Context context, AttributeSet attrs, int defStyle) {
108         super(context, attrs, defStyle);
109     }
110 
111     @Override
onTuningChanged(String key, String newValue)112     public void onTuningChanged(String key, String newValue) {
113         if (!StatusBarIconController.ICON_BLACKLIST.equals(key)) {
114             return;
115         }
116         ArraySet<String> blockList = StatusBarIconController.getIconBlacklist(newValue);
117         boolean blockAirplane = blockList.contains(SLOT_AIRPLANE);
118         boolean blockMobile = blockList.contains(SLOT_MOBILE);
119         boolean blockWifi = blockList.contains(SLOT_WIFI);
120         boolean blockEthernet = blockList.contains(SLOT_ETHERNET);
121 
122         if (blockAirplane != mBlockAirplane || blockMobile != mBlockMobile
123                 || blockEthernet != mBlockEthernet || blockWifi != mBlockWifi) {
124             mBlockAirplane = blockAirplane;
125             mBlockMobile = blockMobile;
126             mBlockEthernet = blockEthernet;
127             mBlockWifi = blockWifi;
128             // Re-register to get new callbacks.
129             mNC.removeSignalCallback(this);
130             mNC.addSignalCallback(this);
131         }
132     }
133 
setNetworkController(NetworkControllerImpl nc)134     public void setNetworkController(NetworkControllerImpl nc) {
135         if (DEBUG) Log.d(TAG, "NetworkController=" + nc);
136         mNC = nc;
137     }
138 
setSecurityController(SecurityController sc)139     public void setSecurityController(SecurityController sc) {
140         if (DEBUG) Log.d(TAG, "SecurityController=" + sc);
141         mSC = sc;
142         mSC.addCallback(this);
143         mVpnVisible = mSC.isVpnEnabled();
144     }
145 
146     @Override
onFinishInflate()147     protected void onFinishInflate() {
148         super.onFinishInflate();
149         mWideTypeIconStartPadding = getContext().getResources().getDimensionPixelSize(
150                 R.dimen.wide_type_icon_start_padding);
151         mSecondaryTelephonyPadding = getContext().getResources().getDimensionPixelSize(
152                 R.dimen.secondary_telephony_padding);
153         mEndPadding = getContext().getResources().getDimensionPixelSize(
154                 R.dimen.signal_cluster_battery_padding);
155         mEndPaddingNothingVisible = getContext().getResources().getDimensionPixelSize(
156                 R.dimen.no_signal_cluster_battery_padding);
157     }
158 
159     @Override
onAttachedToWindow()160     protected void onAttachedToWindow() {
161         super.onAttachedToWindow();
162 
163         mVpn            = (ImageView) findViewById(R.id.vpn);
164         mEthernetGroup  = (ViewGroup) findViewById(R.id.ethernet_combo);
165         mEthernet       = (ImageView) findViewById(R.id.ethernet);
166         mEthernetDark   = (ImageView) findViewById(R.id.ethernet_dark);
167         mWifiGroup      = (ViewGroup) findViewById(R.id.wifi_combo);
168         mWifi           = (ImageView) findViewById(R.id.wifi_signal);
169         mWifiDark       = (ImageView) findViewById(R.id.wifi_signal_dark);
170         mAirplane       = (ImageView) findViewById(R.id.airplane);
171         mNoSims         = (ImageView) findViewById(R.id.no_sims);
172         mNoSimsDark     = (ImageView) findViewById(R.id.no_sims_dark);
173         mNoSimsCombo    =             findViewById(R.id.no_sims_combo);
174         mWifiAirplaneSpacer =         findViewById(R.id.wifi_airplane_spacer);
175         mWifiSignalSpacer =           findViewById(R.id.wifi_signal_spacer);
176         mMobileSignalGroup = (LinearLayout) findViewById(R.id.mobile_signal_group);
177         for (PhoneState state : mPhoneStates) {
178             mMobileSignalGroup.addView(state.mMobileGroup);
179         }
180         TunerService.get(mContext).addTunable(this, StatusBarIconController.ICON_BLACKLIST);
181 
182         apply();
183         applyIconTint();
184     }
185 
186     @Override
onDetachedFromWindow()187     protected void onDetachedFromWindow() {
188         mVpn            = null;
189         mEthernetGroup  = null;
190         mEthernet       = null;
191         mWifiGroup      = null;
192         mWifi           = null;
193         mAirplane       = null;
194         mMobileSignalGroup.removeAllViews();
195         mMobileSignalGroup = null;
196         TunerService.get(mContext).removeTunable(this);
197 
198         super.onDetachedFromWindow();
199     }
200 
201     // From SecurityController.
202     @Override
onStateChanged()203     public void onStateChanged() {
204         post(new Runnable() {
205             @Override
206             public void run() {
207                 mVpnVisible = mSC.isVpnEnabled();
208                 apply();
209             }
210         });
211     }
212 
213     @Override
setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon, boolean activityIn, boolean activityOut, String description)214     public void setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon,
215             boolean activityIn, boolean activityOut, String description) {
216         mWifiVisible = statusIcon.visible && !mBlockWifi;
217         mWifiStrengthId = statusIcon.icon;
218         mWifiDescription = statusIcon.contentDescription;
219 
220         apply();
221     }
222 
223     @Override
setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType, int qsType, boolean activityIn, boolean activityOut, String typeContentDescription, String description, boolean isWide, int subId)224     public void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType,
225             int qsType, boolean activityIn, boolean activityOut, String typeContentDescription,
226             String description, boolean isWide, int subId) {
227         PhoneState state = getState(subId);
228         if (state == null) {
229             return;
230         }
231         state.mMobileVisible = statusIcon.visible && !mBlockMobile;
232         state.mMobileStrengthId = statusIcon.icon;
233         state.mMobileTypeId = statusType;
234         state.mMobileDescription = statusIcon.contentDescription;
235         state.mMobileTypeDescription = typeContentDescription;
236         state.mIsMobileTypeIconWide = statusType != 0 && isWide;
237 
238         apply();
239     }
240 
241     @Override
setEthernetIndicators(IconState state)242     public void setEthernetIndicators(IconState state) {
243         mEthernetVisible = state.visible && !mBlockEthernet;
244         mEthernetIconId = state.icon;
245         mEthernetDescription = state.contentDescription;
246 
247         apply();
248     }
249 
250     @Override
setNoSims(boolean show)251     public void setNoSims(boolean show) {
252         mNoSimsVisible = show && !mBlockMobile;
253     }
254 
255     @Override
setSubs(List<SubscriptionInfo> subs)256     public void setSubs(List<SubscriptionInfo> subs) {
257         if (hasCorrectSubs(subs)) {
258             return;
259         }
260         // Clear out all old subIds.
261         mPhoneStates.clear();
262         if (mMobileSignalGroup != null) {
263             mMobileSignalGroup.removeAllViews();
264         }
265         final int n = subs.size();
266         for (int i = 0; i < n; i++) {
267             inflatePhoneState(subs.get(i).getSubscriptionId());
268         }
269         if (isAttachedToWindow()) {
270             applyIconTint();
271         }
272     }
273 
hasCorrectSubs(List<SubscriptionInfo> subs)274     private boolean hasCorrectSubs(List<SubscriptionInfo> subs) {
275         final int N = subs.size();
276         if (N != mPhoneStates.size()) {
277             return false;
278         }
279         for (int i = 0; i < N; i++) {
280             if (mPhoneStates.get(i).mSubId != subs.get(i).getSubscriptionId()) {
281                 return false;
282             }
283         }
284         return true;
285     }
286 
getState(int subId)287     private PhoneState getState(int subId) {
288         for (PhoneState state : mPhoneStates) {
289             if (state.mSubId == subId) {
290                 return state;
291             }
292         }
293         Log.e(TAG, "Unexpected subscription " + subId);
294         return null;
295     }
296 
inflatePhoneState(int subId)297     private PhoneState inflatePhoneState(int subId) {
298         PhoneState state = new PhoneState(subId, mContext);
299         if (mMobileSignalGroup != null) {
300             mMobileSignalGroup.addView(state.mMobileGroup);
301         }
302         mPhoneStates.add(state);
303         return state;
304     }
305 
306     @Override
setIsAirplaneMode(IconState icon)307     public void setIsAirplaneMode(IconState icon) {
308         mIsAirplaneMode = icon.visible && !mBlockAirplane;
309         mAirplaneIconId = icon.icon;
310         mAirplaneContentDescription = icon.contentDescription;
311 
312         apply();
313     }
314 
315     @Override
setMobileDataEnabled(boolean enabled)316     public void setMobileDataEnabled(boolean enabled) {
317         // Don't care.
318     }
319 
320     @Override
dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event)321     public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
322         // Standard group layout onPopulateAccessibilityEvent() implementations
323         // ignore content description, so populate manually
324         if (mEthernetVisible && mEthernetGroup != null &&
325                 mEthernetGroup.getContentDescription() != null)
326             event.getText().add(mEthernetGroup.getContentDescription());
327         if (mWifiVisible && mWifiGroup != null && mWifiGroup.getContentDescription() != null)
328             event.getText().add(mWifiGroup.getContentDescription());
329         for (PhoneState state : mPhoneStates) {
330             state.populateAccessibilityEvent(event);
331         }
332         return super.dispatchPopulateAccessibilityEventInternal(event);
333     }
334 
335     @Override
onRtlPropertiesChanged(int layoutDirection)336     public void onRtlPropertiesChanged(int layoutDirection) {
337         super.onRtlPropertiesChanged(layoutDirection);
338 
339         if (mEthernet != null) {
340             mEthernet.setImageDrawable(null);
341             mEthernetDark.setImageDrawable(null);
342             mLastEthernetIconId = -1;
343         }
344 
345         if (mWifi != null) {
346             mWifi.setImageDrawable(null);
347             mWifiDark.setImageDrawable(null);
348             mLastWifiStrengthId = -1;
349         }
350 
351         for (PhoneState state : mPhoneStates) {
352             if (state.mMobile != null) {
353                 state.mMobile.setImageDrawable(null);
354             }
355             if (state.mMobileType != null) {
356                 state.mMobileType.setImageDrawable(null);
357             }
358         }
359 
360         if (mAirplane != null) {
361             mAirplane.setImageDrawable(null);
362             mLastAirplaneIconId = -1;
363         }
364 
365         apply();
366     }
367 
368     @Override
hasOverlappingRendering()369     public boolean hasOverlappingRendering() {
370         return false;
371     }
372 
373     // Run after each indicator change.
apply()374     private void apply() {
375         if (mWifiGroup == null) return;
376 
377         mVpn.setVisibility(mVpnVisible ? View.VISIBLE : View.GONE);
378         if (DEBUG) Log.d(TAG, String.format("vpn: %s", mVpnVisible ? "VISIBLE" : "GONE"));
379 
380         if (mEthernetVisible) {
381             if (mLastEthernetIconId != mEthernetIconId) {
382                 mEthernet.setImageResource(mEthernetIconId);
383                 mEthernetDark.setImageResource(mEthernetIconId);
384                 mLastEthernetIconId = mEthernetIconId;
385             }
386             mEthernetGroup.setContentDescription(mEthernetDescription);
387             mEthernetGroup.setVisibility(View.VISIBLE);
388         } else {
389             mEthernetGroup.setVisibility(View.GONE);
390         }
391 
392         if (DEBUG) Log.d(TAG,
393                 String.format("ethernet: %s",
394                     (mEthernetVisible ? "VISIBLE" : "GONE")));
395 
396 
397         if (mWifiVisible) {
398             if (mWifiStrengthId != mLastWifiStrengthId) {
399                 mWifi.setImageResource(mWifiStrengthId);
400                 mWifiDark.setImageResource(mWifiStrengthId);
401                 mLastWifiStrengthId = mWifiStrengthId;
402             }
403             mWifiGroup.setContentDescription(mWifiDescription);
404             mWifiGroup.setVisibility(View.VISIBLE);
405         } else {
406             mWifiGroup.setVisibility(View.GONE);
407         }
408 
409         if (DEBUG) Log.d(TAG,
410                 String.format("wifi: %s sig=%d",
411                     (mWifiVisible ? "VISIBLE" : "GONE"),
412                     mWifiStrengthId));
413 
414         boolean anyMobileVisible = false;
415         int firstMobileTypeId = 0;
416         for (PhoneState state : mPhoneStates) {
417             if (state.apply(anyMobileVisible)) {
418                 if (!anyMobileVisible) {
419                     firstMobileTypeId = state.mMobileTypeId;
420                     anyMobileVisible = true;
421                 }
422             }
423         }
424 
425         if (mIsAirplaneMode) {
426             if (mLastAirplaneIconId != mAirplaneIconId) {
427                 mAirplane.setImageResource(mAirplaneIconId);
428                 mLastAirplaneIconId = mAirplaneIconId;
429             }
430             mAirplane.setContentDescription(mAirplaneContentDescription);
431             mAirplane.setVisibility(View.VISIBLE);
432         } else {
433             mAirplane.setVisibility(View.GONE);
434         }
435 
436         if (mIsAirplaneMode && mWifiVisible) {
437             mWifiAirplaneSpacer.setVisibility(View.VISIBLE);
438         } else {
439             mWifiAirplaneSpacer.setVisibility(View.GONE);
440         }
441 
442         if (((anyMobileVisible && firstMobileTypeId != 0) || mNoSimsVisible) && mWifiVisible) {
443             mWifiSignalSpacer.setVisibility(View.VISIBLE);
444         } else {
445             mWifiSignalSpacer.setVisibility(View.GONE);
446         }
447 
448         mNoSimsCombo.setVisibility(mNoSimsVisible ? View.VISIBLE : View.GONE);
449 
450         boolean anythingVisible = mNoSimsVisible || mWifiVisible || mIsAirplaneMode
451                 || anyMobileVisible || mVpnVisible || mEthernetVisible;
452         setPaddingRelative(0, 0, anythingVisible ? mEndPadding : mEndPaddingNothingVisible, 0);
453     }
454 
setIconTint(int tint, float darkIntensity)455     public void setIconTint(int tint, float darkIntensity) {
456         boolean changed = tint != mIconTint || darkIntensity != mDarkIntensity;
457         mIconTint = tint;
458         mDarkIntensity = darkIntensity;
459         if (changed && isAttachedToWindow()) {
460             applyIconTint();
461         }
462     }
463 
applyIconTint()464     private void applyIconTint() {
465         setTint(mVpn, mIconTint);
466         setTint(mAirplane, mIconTint);
467         applyDarkIntensity(mDarkIntensity, mNoSims, mNoSimsDark);
468         applyDarkIntensity(mDarkIntensity, mWifi, mWifiDark);
469         applyDarkIntensity(mDarkIntensity, mEthernet, mEthernetDark);
470         for (int i = 0; i < mPhoneStates.size(); i++) {
471             mPhoneStates.get(i).setIconTint(mIconTint, mDarkIntensity);
472         }
473     }
474 
applyDarkIntensity(float darkIntensity, View lightIcon, View darkIcon)475     private void applyDarkIntensity(float darkIntensity, View lightIcon, View darkIcon) {
476         lightIcon.setAlpha(1 - darkIntensity);
477         darkIcon.setAlpha(darkIntensity);
478     }
479 
setTint(ImageView v, int tint)480     private void setTint(ImageView v, int tint) {
481         v.setImageTintList(ColorStateList.valueOf(tint));
482     }
483 
484     private class PhoneState {
485         private final int mSubId;
486         private boolean mMobileVisible = false;
487         private int mMobileStrengthId = 0, mMobileTypeId = 0;
488         private boolean mIsMobileTypeIconWide;
489         private String mMobileDescription, mMobileTypeDescription;
490 
491         private ViewGroup mMobileGroup;
492         private ImageView mMobile, mMobileDark, mMobileType;
493 
PhoneState(int subId, Context context)494         public PhoneState(int subId, Context context) {
495             ViewGroup root = (ViewGroup) LayoutInflater.from(context)
496                     .inflate(R.layout.mobile_signal_group, null);
497             setViews(root);
498             mSubId = subId;
499         }
500 
setViews(ViewGroup root)501         public void setViews(ViewGroup root) {
502             mMobileGroup    = root;
503             mMobile         = (ImageView) root.findViewById(R.id.mobile_signal);
504             mMobileDark     = (ImageView) root.findViewById(R.id.mobile_signal_dark);
505             mMobileType     = (ImageView) root.findViewById(R.id.mobile_type);
506         }
507 
apply(boolean isSecondaryIcon)508         public boolean apply(boolean isSecondaryIcon) {
509             if (mMobileVisible && !mIsAirplaneMode) {
510                 mMobile.setImageResource(mMobileStrengthId);
511                 Drawable mobileDrawable = mMobile.getDrawable();
512                 if (mobileDrawable instanceof Animatable) {
513                     Animatable ad = (Animatable) mobileDrawable;
514                     if (!ad.isRunning()) {
515                         ad.start();
516                     }
517                 }
518 
519                 mMobileDark.setImageResource(mMobileStrengthId);
520                 Drawable mobileDarkDrawable = mMobileDark.getDrawable();
521                 if (mobileDarkDrawable instanceof Animatable) {
522                     Animatable ad = (Animatable) mobileDarkDrawable;
523                     if (!ad.isRunning()) {
524                         ad.start();
525                     }
526                 }
527 
528                 mMobileType.setImageResource(mMobileTypeId);
529                 mMobileGroup.setContentDescription(mMobileTypeDescription
530                         + " " + mMobileDescription);
531                 mMobileGroup.setVisibility(View.VISIBLE);
532             } else {
533                 mMobileGroup.setVisibility(View.GONE);
534             }
535 
536             // When this isn't next to wifi, give it some extra padding between the signals.
537             mMobileGroup.setPaddingRelative(isSecondaryIcon ? mSecondaryTelephonyPadding : 0,
538                     0, 0, 0);
539             mMobile.setPaddingRelative(mIsMobileTypeIconWide ? mWideTypeIconStartPadding : 0,
540                     0, 0, 0);
541             mMobileDark.setPaddingRelative(mIsMobileTypeIconWide ? mWideTypeIconStartPadding : 0,
542                     0, 0, 0);
543 
544             if (DEBUG) Log.d(TAG, String.format("mobile: %s sig=%d typ=%d",
545                         (mMobileVisible ? "VISIBLE" : "GONE"), mMobileStrengthId, mMobileTypeId));
546 
547             mMobileType.setVisibility(mMobileTypeId != 0 ? View.VISIBLE : View.GONE);
548 
549             return mMobileVisible;
550         }
551 
populateAccessibilityEvent(AccessibilityEvent event)552         public void populateAccessibilityEvent(AccessibilityEvent event) {
553             if (mMobileVisible && mMobileGroup != null
554                     && mMobileGroup.getContentDescription() != null) {
555                 event.getText().add(mMobileGroup.getContentDescription());
556             }
557         }
558 
setIconTint(int tint, float darkIntensity)559         public void setIconTint(int tint, float darkIntensity) {
560             applyDarkIntensity(darkIntensity, mMobile, mMobileDark);
561             setTint(mMobileType, tint);
562         }
563     }
564 }
565 
566