1 /*
2  * Copyright (C) 2022 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.settingslib.mobile
18 
19 import android.annotation.DrawableRes
20 import android.content.res.Resources
21 import android.content.res.TypedArray
22 import android.util.Log
23 import androidx.annotation.VisibleForTesting
24 import com.android.settingslib.R
25 import com.android.settingslib.SignalIcon.MobileIconGroup
26 
27 /**
28  * This class defines a network type (3G, 4G, etc.) override mechanism on a per-carrierId basis.
29  *
30  * Traditionally, carrier-customized network type iconography was achieved using the `MCC/MNC`
31  * resource qualifiers, and swapping out the drawable resource by name. It would look like this:
32  *
33  *     res/
34  *       drawable/
35  *         3g_mobiledata_icon.xml
36  *       drawable-MCC-MNC/
37  *         3g_mobiledata_icon.xml
38  *
39  * This would mean that, provided a context created with this MCC/MNC configuration set, loading
40  * the network type icon through [MobileIconGroup] would provide a carrier-defined network type
41  * icon rather than the AOSP-defined default.
42  *
43  * The MCC/MNC mechanism no longer can fully define carrier-specific network type icons, because
44  * there is no longer a 1:1 mapping between MCC/MNC and carrier. With the advent of MVNOs, multiple
45  * carriers can have the same MCC/MNC value, but wish to differentiate based on their carrier ID.
46  * CarrierId is a newer concept than MCC/MNC, and provides more granularity when it comes to
47  * determining the carrier (e.g. MVNOs can share MCC/MNC values with the network owner), therefore
48  * it can fit all of the same use cases currently handled by `MCC/MNC`, without the need to apply a
49  * configuration context in order to get the proper UI for a given SIM icon.
50  *
51  * NOTE: CarrierId icon overrides will always take precedence over those defined using `MCC/MNC`
52  * resource qualifiers.
53  *
54  * [MAPPING] encodes the relationship between CarrierId and the corresponding override array
55  * that exists in the config.xml. An alternative approach could be to generate the resource name
56  * by string concatenation at run-time:
57  *
58  *    val resName = "carrierId_$carrierId_iconOverrides"
59  *    val override = resources.getResourceIdentifier(resName)
60  *
61  * However, that's going to be far less efficient until MAPPING grows to a sufficient size. For now,
62  * given a relatively small number of entries, we should just maintain the mapping here.
63  */
64 interface MobileIconCarrierIdOverrides {
65     @DrawableRes
getOverrideFornull66     fun getOverrideFor(carrierId: Int, networkType: String, resources: Resources): Int
67     fun carrierIdEntryExists(carrierId: Int): Boolean
68 }
69 
70 class MobileIconCarrierIdOverridesImpl : MobileIconCarrierIdOverrides {
71     @DrawableRes
72     override fun getOverrideFor(carrierId: Int, networkType: String, resources: Resources): Int {
73         val resId = MAPPING[carrierId] ?: return 0
74         val ta = resources.obtainTypedArray(resId)
75         val map = parseNetworkIconOverrideTypedArray(ta)
76         ta.recycle()
77         return map[networkType] ?: 0
78     }
79 
80     override fun carrierIdEntryExists(carrierId: Int) =
81         overrideExists(carrierId, MAPPING)
82 
83     companion object {
84         private const val TAG = "MobileIconOverrides"
85         /**
86          * This map maintains the lookup from the canonical carrier ID (see below link) to the
87          * corresponding overlay resource. New overrides should add an entry below in order to
88          * change the network type icon resources based on carrier ID
89          *
90          * Refer to the link below for the canonical mapping maintained in AOSP:
91          * https://android.googlesource.com/platform/packages/providers/TelephonyProvider/+/master/assets/latest_carrier_id/carrier_list.textpb
92          */
93         private val MAPPING = mapOf(
94             // 2032 == Xfinity Mobile
95             2032 to R.array.carrierId_2032_iconOverrides,
96         )
97 
98         /**
99          * Parse `carrierId_XXXX_iconOverrides` for a particular network type. The resource file
100          * "carrierid_icon_overrides.xml" defines a TypedArray format for overriding specific
101          * network type icons (a.k.a. RAT icons) for a particular carrier ID. The format is defined
102          * as an array of (network type name, drawable) pairs:
103          *    <array name="carrierId_XXXX_iconOverrides>
104          *        <item>NET_TYPE_1</item>
105          *        <item>@drawable/net_type_1_override</item>
106          *        <item>NET_TYPE_2</item>
107          *        <item>@drawable/net_type_2_override</item>
108          *    </array>
109          *
110          * @param ta the [TypedArray] defined in carrierid_icon_overrides.xml
111          * @return the overridden drawable resource ID if it exists, or 0 if it does not
112          */
113         @VisibleForTesting
114         @JvmStatic
115         fun parseNetworkIconOverrideTypedArray(ta: TypedArray): Map<String, Int> {
116             if (ta.length() % 2 != 0) {
117                 Log.w(TAG,
118                     "override must contain an even number of (key, value) entries. skipping")
119 
120                 return mapOf()
121             }
122 
123             val result = mutableMapOf<String, Int>()
124             // The array is defined as Pair(String, resourceId), so walk by 2
125             for (i in 0 until ta.length() step 2) {
126                 val key = ta.getString(i)
127                 val override = ta.getResourceId(i + 1, 0)
128                 if (key == null || override == 0) {
129                     Log.w(TAG, "Invalid override found. Skipping")
130                     continue
131                 }
132                 result[key] = override
133             }
134 
135             return result
136         }
137 
138         @JvmStatic
139         private fun overrideExists(carrierId: Int, mapping: Map<Int, Int>): Boolean =
140             mapping.containsKey(carrierId)
141     }
142 }
143