1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.settings.datausage;
16 
17 import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND;
18 
19 import static com.android.settings.datausage.lib.AppDataUsageRepository.getAppUid;
20 import static com.android.settings.datausage.lib.AppDataUsageRepository.getAppUidList;
21 
22 import android.app.Activity;
23 import android.app.settings.SettingsEnums;
24 import android.content.Context;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.PackageManager;
27 import android.graphics.drawable.Drawable;
28 import android.net.NetworkTemplate;
29 import android.os.Bundle;
30 import android.os.UserHandle;
31 import android.util.ArraySet;
32 import android.util.IconDrawableFactory;
33 import android.util.Log;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.VisibleForTesting;
37 import androidx.preference.Preference;
38 import androidx.preference.Preference.OnPreferenceChangeListener;
39 import androidx.recyclerview.widget.DefaultItemAnimator;
40 import androidx.recyclerview.widget.RecyclerView;
41 
42 import com.android.settings.R;
43 import com.android.settings.applications.AppInfoBase;
44 import com.android.settings.datausage.lib.AppDataUsageDetailsRepository;
45 import com.android.settings.datausage.lib.NetworkTemplates;
46 import com.android.settings.fuelgauge.datasaver.DynamicDenylistManager;
47 import com.android.settings.network.SubscriptionUtil;
48 import com.android.settings.widget.EntityHeaderController;
49 import com.android.settingslib.AppItem;
50 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
51 import com.android.settingslib.RestrictedLockUtilsInternal;
52 import com.android.settingslib.RestrictedSwitchPreference;
53 import com.android.settingslib.net.UidDetail;
54 import com.android.settingslib.net.UidDetailProvider;
55 
56 import kotlin.Unit;
57 
58 import java.util.ArrayList;
59 import java.util.Collections;
60 import java.util.List;
61 
62 public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceChangeListener,
63         DataSaverBackend.Listener {
64 
65     private static final String TAG = "AppDataUsage";
66 
67     static final String ARG_APP_ITEM = "app_item";
68     static final String ARG_NETWORK_TEMPLATE = "network_template";
69     static final String ARG_NETWORK_CYCLES = "network_cycles";
70     static final String ARG_SELECTED_CYCLE = "selected_cycle";
71 
72     private static final String KEY_RESTRICT_BACKGROUND = "restrict_background";
73     private static final String KEY_UNRESTRICTED_DATA = "unrestricted_data_saver";
74 
75     private PackageManager mPackageManager;
76     private final ArraySet<String> mPackages = new ArraySet<>();
77     private RestrictedSwitchPreference mRestrictBackground;
78 
79     private Drawable mIcon;
80     @VisibleForTesting
81     CharSequence mLabel;
82     @VisibleForTesting
83     String mPackageName;
84 
85     @VisibleForTesting
86     NetworkTemplate mTemplate;
87     private AppItem mAppItem;
88     private RestrictedSwitchPreference mUnrestrictedData;
89     private DataSaverBackend mDataSaverBackend;
90     private Context mContext;
91     private ArrayList<Long> mCycles;
92     private long mSelectedCycle;
93     private boolean mIsLoading;
94 
isSimHardwareVisible(Context context)95     public boolean isSimHardwareVisible(Context context) {
96         return SubscriptionUtil.isSimHardwareVisible(context);
97     }
98 
99     @Override
onCreate(Bundle icicle)100     public void onCreate(Bundle icicle) {
101         super.onCreate(icicle);
102         mContext = getContext();
103         mPackageManager = getPackageManager();
104         final Bundle args = getArguments();
105 
106         mAppItem = (args != null) ? (AppItem) args.getParcelable(ARG_APP_ITEM) : null;
107         mTemplate = (args != null) ? (NetworkTemplate) args.getParcelable(ARG_NETWORK_TEMPLATE)
108                 : null;
109         mCycles = (args != null) ? (ArrayList) args.getSerializable(ARG_NETWORK_CYCLES)
110             : null;
111         mSelectedCycle = (args != null) ? args.getLong(ARG_SELECTED_CYCLE) : 0L;
112 
113         if (mTemplate == null) {
114             mTemplate = NetworkTemplates.INSTANCE.getDefaultTemplate(mContext);
115         }
116         final Activity activity = requireActivity();
117         activity.setTitle(NetworkTemplates.getTitleResId(mTemplate));
118         if (mAppItem == null) {
119             int uid = (args != null) ? args.getInt(AppInfoBase.ARG_PACKAGE_UID, -1)
120                     : getActivity().getIntent().getIntExtra(AppInfoBase.ARG_PACKAGE_UID, -1);
121             if (uid == -1) {
122                 // TODO: Log error.
123                 activity.finish();
124             } else {
125                 addUid(uid);
126                 mAppItem = new AppItem(uid);
127                 mAppItem.addUid(uid);
128             }
129         } else {
130             for (int i = 0; i < mAppItem.uids.size(); i++) {
131                 addUid(mAppItem.uids.keyAt(i));
132             }
133         }
134 
135         final List<Integer> uidList = getAppUidList(mAppItem.uids);
136         initCycle(uidList);
137 
138         final UidDetailProvider uidDetailProvider = getUidDetailProvider();
139 
140         if (mAppItem.key > 0) {
141             if ((!isSimHardwareVisible(mContext)) || !UserHandle.isApp(mAppItem.key)) {
142                 final UidDetail uidDetail = uidDetailProvider.getUidDetail(mAppItem.key, true);
143                 mIcon = uidDetail.icon;
144                 mLabel = uidDetail.label;
145                 removePreference(KEY_UNRESTRICTED_DATA);
146                 removePreference(KEY_RESTRICT_BACKGROUND);
147             } else {
148                 if (mPackages.size() != 0) {
149                     int userId = UserHandle.getUserId(mAppItem.key);
150                     try {
151                         final ApplicationInfo info = mPackageManager.getApplicationInfoAsUser(
152                                 mPackages.valueAt(0), 0, userId);
153                         mIcon = IconDrawableFactory.newInstance(getActivity()).getBadgedIcon(info);
154                         mLabel = info.loadLabel(mPackageManager);
155                         mPackageName = info.packageName;
156                     } catch (PackageManager.NameNotFoundException e) {
157                     }
158                     use(AppDataUsageAppSettingsController.class).init(mPackages, userId);
159                 }
160                 mRestrictBackground = findPreference(KEY_RESTRICT_BACKGROUND);
161                 mRestrictBackground.setOnPreferenceChangeListener(this);
162                 mUnrestrictedData = findPreference(KEY_UNRESTRICTED_DATA);
163                 mUnrestrictedData.setOnPreferenceChangeListener(this);
164             }
165             mDataSaverBackend = new DataSaverBackend(mContext);
166 
167             use(AppDataUsageListController.class).init(uidList);
168         } else {
169             final Context context = getActivity();
170             final UidDetail uidDetail = uidDetailProvider.getUidDetail(mAppItem.key, true);
171             mIcon = uidDetail.icon;
172             mLabel = uidDetail.label;
173             mPackageName = context.getPackageName();
174 
175             removePreference(KEY_UNRESTRICTED_DATA);
176             removePreference(KEY_RESTRICT_BACKGROUND);
177         }
178 
179         addEntityHeader();
180     }
181 
182     @Override
onStart()183     public void onStart() {
184         super.onStart();
185         // No animations will occur before bindData() initially updates the cycle.
186         // This is mainly for the cycle spinner, because when the page is entered from the
187         // AppInfoDashboardFragment, there is no way to know whether the cycle data is available
188         // before finished the async loading.
189         // The animator will be set back if any page updates happens after loading, in
190         // setBackPreferenceListAnimatorIfLoaded().
191         mIsLoading = true;
192         getListView().setItemAnimator(null);
193     }
194 
195     @Override
onResume()196     public void onResume() {
197         super.onResume();
198         if (mDataSaverBackend != null) {
199             mDataSaverBackend.addListener(this);
200         }
201         updatePrefs();
202     }
203 
204     @Override
onPause()205     public void onPause() {
206         super.onPause();
207         if (mDataSaverBackend != null) {
208             mDataSaverBackend.remListener(this);
209         }
210     }
211 
212     @Override
onPreferenceChange(@onNull Preference preference, Object newValue)213     public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
214         if (preference == mRestrictBackground) {
215             mDataSaverBackend.setIsDenylisted(mAppItem.key, mPackageName, !(Boolean) newValue);
216             updatePrefs();
217             return true;
218         } else if (preference == mUnrestrictedData) {
219             mDataSaverBackend.setIsAllowlisted(mAppItem.key, mPackageName, (Boolean) newValue);
220             return true;
221         }
222         return false;
223     }
224 
225     @Override
getPreferenceScreenResId()226     protected int getPreferenceScreenResId() {
227         return R.xml.app_data_usage;
228     }
229 
230     @Override
getLogTag()231     protected String getLogTag() {
232         return TAG;
233     }
234 
235     @VisibleForTesting
updatePrefs()236     void updatePrefs() {
237         updatePrefs(getAppRestrictBackground(), getUnrestrictData());
238     }
239 
240     @VisibleForTesting
getUidDetailProvider()241     UidDetailProvider getUidDetailProvider() {
242         return new UidDetailProvider(mContext);
243     }
244 
245     @VisibleForTesting
initCycle(List<Integer> uidList)246     void initCycle(List<Integer> uidList) {
247         var cycleController = use(AppDataUsageCycleController.class);
248         var summaryController = use(AppDataUsageSummaryController.class);
249         var repository = new AppDataUsageDetailsRepository(mContext, mTemplate, mCycles, uidList);
250         cycleController.init(repository, data -> {
251             mIsLoading = false;
252             summaryController.update(data);
253             return Unit.INSTANCE;
254         });
255         if (mCycles != null) {
256             Log.d(TAG, "setInitialCycles: " + mCycles + " " + mSelectedCycle);
257             cycleController.setInitialCycles(mCycles, mSelectedCycle);
258         }
259     }
260 
261     /**
262      * Sets back the preference list's animator if the loading is finished.
263      *
264      * The preference list's animator was temporarily removed before loading in onResume().
265      * When need to update the preference visibility in this page after the loading, adding the
266      * animator back to keeping the usual animations.
267      */
setBackPreferenceListAnimatorIfLoaded()268     private void setBackPreferenceListAnimatorIfLoaded() {
269         if (mIsLoading) {
270             return;
271         }
272         RecyclerView recyclerView = getListView();
273         if (recyclerView.getItemAnimator() == null) {
274             recyclerView.setItemAnimator(new DefaultItemAnimator());
275         }
276     }
277 
updatePrefs(boolean restrictBackground, boolean unrestrictData)278     private void updatePrefs(boolean restrictBackground, boolean unrestrictData) {
279         if (!isSimHardwareVisible(mContext)) {
280             return;
281         }
282         setBackPreferenceListAnimatorIfLoaded();
283         final EnforcedAdmin admin = RestrictedLockUtilsInternal
284                 .checkIfMeteredDataUsageUserControlDisabled(mContext, mPackageName,
285                         UserHandle.getUserId(mAppItem.key));
286         if (mRestrictBackground != null) {
287             mRestrictBackground.setChecked(!restrictBackground);
288             mRestrictBackground.setDisabledByAdmin(admin);
289         }
290         if (mUnrestrictedData != null) {
291             if (restrictBackground) {
292                 mUnrestrictedData.setVisible(false);
293             } else {
294                 mUnrestrictedData.setVisible(true);
295                 mUnrestrictedData.setChecked(unrestrictData);
296                 mUnrestrictedData.setDisabledByAdmin(admin);
297             }
298         }
299     }
300 
addUid(int uid)301     private void addUid(int uid) {
302         String[] packages = mPackageManager.getPackagesForUid(getAppUid(uid));
303         if (packages != null) {
304             Collections.addAll(mPackages, packages);
305         }
306     }
307 
getAppRestrictBackground()308     private boolean getAppRestrictBackground() {
309         final int uid = mAppItem.key;
310         final int uidPolicy = services.mPolicyManager.getUidPolicy(uid);
311         return (uidPolicy & POLICY_REJECT_METERED_BACKGROUND) != 0
312                 && DynamicDenylistManager.getInstance(mContext).isInManualDenylist(uid);
313     }
314 
getUnrestrictData()315     private boolean getUnrestrictData() {
316         if (mDataSaverBackend != null) {
317             return mDataSaverBackend.isAllowlisted(mAppItem.key);
318         }
319         return false;
320     }
321 
322     @VisibleForTesting
addEntityHeader()323     void addEntityHeader() {
324         String pkg = mPackages.size() != 0 ? mPackages.valueAt(0) : null;
325         int uid = 0;
326         if (pkg != null) {
327             try {
328                 uid = mPackageManager.getPackageUidAsUser(pkg,
329                         UserHandle.getUserId(mAppItem.key));
330             } catch (PackageManager.NameNotFoundException e) {
331                 Log.w(TAG, "Skipping UID because cannot find package " + pkg);
332             }
333         }
334 
335         final boolean showInfoButton = mAppItem.key > 0;
336 
337         final Activity activity = getActivity();
338         final Preference pref = EntityHeaderController
339                 .newInstance(activity, this, null /* header */)
340                 .setUid(uid)
341                 .setHasAppInfoLink(showInfoButton)
342                 .setButtonActions(EntityHeaderController.ActionType.ACTION_NONE,
343                         EntityHeaderController.ActionType.ACTION_NONE)
344                 .setIcon(mIcon)
345                 .setLabel(mLabel)
346                 .setPackageName(pkg)
347                 .done(getPrefContext());
348         getPreferenceScreen().addPreference(pref);
349     }
350 
351     @Override
getMetricsCategory()352     public int getMetricsCategory() {
353         return SettingsEnums.APP_DATA_USAGE;
354     }
355 
356     @Override
onDataSaverChanged(boolean isDataSaving)357     public void onDataSaverChanged(boolean isDataSaving) {
358 
359     }
360 
361     @Override
onAllowlistStatusChanged(int uid, boolean isAllowlisted)362     public void onAllowlistStatusChanged(int uid, boolean isAllowlisted) {
363         if (mAppItem.uids.get(uid, false)) {
364             updatePrefs(getAppRestrictBackground(), isAllowlisted);
365         }
366     }
367 
368     @Override
onDenylistStatusChanged(int uid, boolean isDenylisted)369     public void onDenylistStatusChanged(int uid, boolean isDenylisted) {
370         if (mAppItem.uids.get(uid, false)) {
371             updatePrefs(isDenylisted, getUnrestrictData());
372         }
373     }
374 }
375