Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d4fd8c0c authored by Chaohui Wang's avatar Chaohui Wang Committed by Android (Google) Code Review
Browse files

Merge "Fix DataUsageSummaryPreferenceController ANR" into main

parents 9abd03fb df5c4f69
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -18,8 +18,8 @@
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="22dp"
    android:paddingBottom="32dp"
    android:paddingTop="8dp"
    android:paddingBottom="16dp"
    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
    android:orientation="vertical"
@@ -99,6 +99,7 @@
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="12dp"
        android:minHeight="54dp"
        android:orientation="vertical">

        <TextView
+50 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.datausage

data class DataPlanInfo(

    /** The number of registered plans, [0, N] */
    val dataPlanCount: Int,

    /**
     * The size of the first registered plan if one exists or the size of the warning if it is set.
     *
     * Set to -1 if no plan information is available.
     */
    val dataPlanSize: Long,

    /**
     * The "size" of the data usage bar, i.e. the amount of data its rhs end represents.
     *
     * Set to -1 if not display a data usage bar.
     */
    val dataBarSize: Long,

    /** The number of bytes used since the start of the cycle. */
    val dataPlanUse: Long,

    /**
     * The ending time of the billing cycle in ms since the epoch.
     *
     * Set to `null` if no cycle information is available.
     */
    val cycleEnd: Long?,

    /** The time of the last update in milliseconds since the epoch, or -1 if unknown. */
    val snapshotTime: Long,
)
+75 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.datausage

import android.net.NetworkPolicy
import android.telephony.SubscriptionPlan
import com.android.settings.datausage.lib.INetworkCycleDataRepository
import com.android.settings.datausage.lib.NetworkCycleDataRepository.Companion.getCycles
import com.android.settings.datausage.lib.NetworkStatsRepository

interface DataPlanRepository {
    fun getDataPlanInfo(policy: NetworkPolicy, plans: List<SubscriptionPlan>): DataPlanInfo
}

class DataPlanRepositoryImpl(
    private val networkCycleDataRepository: INetworkCycleDataRepository,
) : DataPlanRepository {
    override fun getDataPlanInfo(
        policy: NetworkPolicy,
        plans: List<SubscriptionPlan>,
    ): DataPlanInfo {
        getPrimaryPlan(plans)?.let { primaryPlan ->
            val dataPlanSize = when (primaryPlan.dataLimitBytes) {
                SubscriptionPlan.BYTES_UNLIMITED -> SubscriptionPlan.BYTES_UNKNOWN
                else -> primaryPlan.dataLimitBytes
            }
            return DataPlanInfo(
                dataPlanCount = plans.size,
                dataPlanSize = dataPlanSize,
                dataBarSize = dataPlanSize,
                dataPlanUse = primaryPlan.dataUsageBytes,
                cycleEnd = primaryPlan.cycleRule.end?.toInstant()?.toEpochMilli(),
                snapshotTime = primaryPlan.dataUsageTime,
            )
        }

        val cycle = policy.getCycles().firstOrNull()
        val dataUsage = networkCycleDataRepository.queryUsage(
            cycle ?: NetworkStatsRepository.AllTimeRange
        ).usage
        return DataPlanInfo(
            dataPlanCount = 0,
            dataPlanSize = SubscriptionPlan.BYTES_UNKNOWN,
            dataBarSize = maxOf(dataUsage, policy.limitBytes, policy.warningBytes),
            dataPlanUse = dataUsage,
            cycleEnd = cycle?.upper,
            snapshotTime = SubscriptionPlan.TIME_UNKNOWN,
        )
    }

    companion object {
        private const val PETA = 1_000_000_000_000_000L

        private fun getPrimaryPlan(plans: List<SubscriptionPlan>): SubscriptionPlan? =
            plans.firstOrNull()?.takeIf { plan ->
                plan.dataLimitBytes > 0 && validSize(plan.dataUsageBytes) && plan.cycleRule != null
            }

        private fun validSize(value: Long): Boolean = value in 0L until PETA
    }
}
+16 −20
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ import android.annotation.AttrRes;
import android.content.Context;
import android.graphics.Typeface;
import android.icu.text.MessageFormat;
import android.net.NetworkTemplate;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
@@ -32,13 +31,14 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;

import com.android.settings.R;
import com.android.settingslib.Utils;
import com.android.settingslib.net.DataUsageController;
import com.android.settingslib.utils.StringUtil;

import java.util.HashMap;
@@ -62,10 +62,9 @@ public class DataUsageSummaryPreference extends Preference {
    private CharSequence mEndLabel;

    private int mNumPlans;
    /** The specified un-initialized value for cycle time */
    private static final long CYCLE_TIME_UNINITIAL_VALUE = 0;
    /** The ending time of the billing cycle in milliseconds since epoch. */
    private long mCycleEndTimeMs;
    @Nullable
    private Long mCycleEndTimeMs;
    /** The time of the last update in standard milliseconds since the epoch */
    private long mSnapshotTimeMs;
    /** Name of carrier, or null if not available */
@@ -74,7 +73,6 @@ public class DataUsageSummaryPreference extends Preference {

    /** Progress to display on ProgressBar */
    private float mProgress;
    private boolean mHasMobileData;

    /**
     * The size of the first registered plan if one exists or the size of the warning if it is set.
@@ -102,7 +100,10 @@ public class DataUsageSummaryPreference extends Preference {
        notifyChanged();
    }

    public void setUsageInfo(long cycleEnd, long snapshotTime, CharSequence carrierName,
    /**
     * Sets the usage info.
     */
    public void setUsageInfo(@Nullable Long cycleEnd, long snapshotTime, CharSequence carrierName,
            int numPlans) {
        mCycleEndTimeMs = cycleEnd;
        mSnapshotTimeMs = snapshotTime;
@@ -124,15 +125,17 @@ public class DataUsageSummaryPreference extends Preference {
        notifyChanged();
    }

    void setUsageNumbers(long used, long dataPlanSize, boolean hasMobileData) {
    /**
     * Sets the usage numbers.
     */
    public void setUsageNumbers(long used, long dataPlanSize) {
        mDataplanUse = used;
        mDataplanSize = dataPlanSize;
        mHasMobileData = hasMobileData;
        notifyChanged();
    }

    @Override
    public void onBindViewHolder(PreferenceViewHolder holder) {
    public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
        super.onBindViewHolder(holder);

        ProgressBar bar = getProgressBar(holder);
@@ -178,7 +181,7 @@ public class DataUsageSummaryPreference extends Preference {

        final MeasurableLinearLayout layout = getLayout(holder);

        if (mHasMobileData && mNumPlans >= 0 && mDataplanSize > 0L) {
        if (mDataplanSize > 0L) {
            TextView usageRemainingField = getDataRemaining(holder);
            long dataRemaining = mDataplanSize - mDataplanUse;
            if (dataRemaining >= 0) {
@@ -204,7 +207,7 @@ public class DataUsageSummaryPreference extends Preference {
        TextView cycleTime = getCycleTime(holder);

        // Takes zero as a special case which value is never set.
        if (mCycleEndTimeMs == CYCLE_TIME_UNINITIAL_VALUE) {
        if (mCycleEndTimeMs == null) {
            cycleTime.setVisibility(View.GONE);
            return;
        }
@@ -228,7 +231,7 @@ public class DataUsageSummaryPreference extends Preference {


    private void updateCarrierInfo(TextView carrierInfo) {
        if (mNumPlans > 0 && mSnapshotTimeMs >= 0L) {
        if (mSnapshotTimeMs >= 0L) {
            carrierInfo.setVisibility(View.VISIBLE);
            long updateAgeMillis = calculateTruncatedUpdateAge();

@@ -293,13 +296,6 @@ public class DataUsageSummaryPreference extends Preference {
        carrierInfo.setTypeface(typeface);
    }

    @VisibleForTesting
    protected long getHistoricalUsageLevel() {
        final DataUsageController controller = new DataUsageController(getContext());
        return controller.getHistoricalUsageLevel(
                new NetworkTemplate.Builder(NetworkTemplate.MATCH_WIFI).build());
    }

    @VisibleForTesting
    protected TextView getUsageTitle(PreferenceViewHolder holder) {
        return (TextView) holder.findViewById(R.id.usage_title);
+0 −273
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.datausage;

import android.app.Activity;
import android.content.Context;
import android.net.NetworkTemplate;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionPlan;
import android.text.TextUtils;
import android.util.Log;
import android.util.RecurrenceRule;

import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;

import com.android.internal.util.CollectionUtils;
import com.android.settings.R;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.datausage.lib.DataUsageLib;
import com.android.settings.network.ProxySubscriptionManager;
import com.android.settings.network.telephony.TelephonyBasePreferenceController;
import com.android.settingslib.net.DataUsageController;
import com.android.settingslib.utils.ThreadUtils;

import java.util.List;
import java.util.concurrent.Future;

/**
 * This is the controller for a data usage header that retrieves carrier data from the new
 * subscriptions framework API if available. The controller reads subscription information from the
 * framework and falls back to legacy usage data if none are available.
 */
public class DataUsageSummaryPreferenceController extends TelephonyBasePreferenceController
        implements PreferenceControllerMixin {

    private static final String TAG = "DataUsageController";
    private static final String KEY = "status_header";
    private static final long PETA = 1000000000000000L;

    protected DataUsageController mDataUsageController;
    protected DataUsageInfoController mDataInfoController;
    private NetworkTemplate mDefaultTemplate;
    private boolean mHasMobileData;

    /** Name of the carrier, or null if not available */
    private CharSequence mCarrierName;

    /** The number of registered plans, [0,N] */
    private int mDataplanCount;

    /** The time of the last update in milliseconds since the epoch, or -1 if unknown */
    private long mSnapshotTime;

    /**
     * The size of the first registered plan if one exists or the size of the warning if it is set.
     * -1 if no information is available.
     */
    private long mDataplanSize;
    /** The "size" of the data usage bar, i.e. the amount of data its rhs end represents */
    private long mDataBarSize;
    /** The number of bytes used since the start of the cycle. */
    private long mDataplanUse;
    /** The ending time of the billing cycle in ms since the epoch */
    private long mCycleEnd;

    private Future<Long> mHistoricalUsageLevel;

    public DataUsageSummaryPreferenceController(Activity activity, int subscriptionId) {
        super(activity, KEY);

        init(subscriptionId);
    }

    /**
     * Initialize based on subscription ID provided
     * @param subscriptionId is the target subscriptionId
     */
    public void init(int subscriptionId) {
        mSubId = subscriptionId;
        mHasMobileData = DataUsageUtils.hasMobileData(mContext);
        mDataUsageController = null;
    }

    protected void updateConfiguration(Context context,
            int subscriptionId, SubscriptionInfo subInfo) {
        mDataUsageController = createDataUsageController(context);
        mDataUsageController.setSubscriptionId(subscriptionId);
        mDataInfoController = new DataUsageInfoController();

        if (subInfo != null) {
            mDefaultTemplate = DataUsageLib.getMobileTemplate(context, subscriptionId);
        }
    }

    @VisibleForTesting
    DataUsageController createDataUsageController(Context context) {
        return new DataUsageController(context);
    }

    @VisibleForTesting
    DataUsageSummaryPreferenceController(
            DataUsageController dataUsageController,
            DataUsageInfoController dataInfoController,
            NetworkTemplate defaultTemplate,
            Activity activity,
            int subscriptionId) {
        super(activity, KEY);
        mDataUsageController = dataUsageController;
        mDataInfoController = dataInfoController;
        mDefaultTemplate = defaultTemplate;
        mHasMobileData = true;
        mSubId = subscriptionId;
    }

    @VisibleForTesting
    List<SubscriptionPlan> getSubscriptionPlans(int subscriptionId) {
        return ProxySubscriptionManager.getInstance(mContext).get()
                .getSubscriptionPlans(subscriptionId);
    }

    protected SubscriptionInfo getSubscriptionInfo(int subscriptionId) {
        if (!mHasMobileData) {
            return null;
        }
        return ProxySubscriptionManager.getInstance(mContext)
                .getAccessibleSubscriptionInfo(subscriptionId);
    }

    @Override
    public int getAvailabilityStatus(int subId) {
        return getSubscriptionInfo(subId) != null ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
    }

    @Override
    public void updateState(Preference preference) {
        DataUsageSummaryPreference summaryPreference = (DataUsageSummaryPreference) preference;

        final SubscriptionInfo subInfo = getSubscriptionInfo(mSubId);
        if (subInfo == null) {
            return;
        }
        if (mDataUsageController == null) {
            updateConfiguration(mContext, mSubId, subInfo);
        }

        mHistoricalUsageLevel = ThreadUtils.postOnBackgroundThread(() ->
                mDataUsageController.getHistoricalUsageLevel(mDefaultTemplate));

        final DataUsageController.DataUsageInfo info =
                mDataUsageController.getDataUsageInfo(mDefaultTemplate);

        long usageLevel = info.usageLevel;

        refreshDataplanInfo(info, subInfo);

        if (info.warningLevel > 0 && info.limitLevel > 0) {
            summaryPreference.setLimitInfo(TextUtils.expandTemplate(
                    mContext.getText(R.string.cell_data_warning_and_limit),
                    DataUsageUtils.formatDataUsage(mContext, info.warningLevel),
                    DataUsageUtils.formatDataUsage(mContext, info.limitLevel)));
        } else if (info.warningLevel > 0) {
            summaryPreference.setLimitInfo(TextUtils.expandTemplate(
                    mContext.getText(R.string.cell_data_warning),
                    DataUsageUtils.formatDataUsage(mContext, info.warningLevel)));
        } else if (info.limitLevel > 0) {
            summaryPreference.setLimitInfo(TextUtils.expandTemplate(
                    mContext.getText(R.string.cell_data_limit),
                    DataUsageUtils.formatDataUsage(mContext, info.limitLevel)));
        } else {
            summaryPreference.setLimitInfo(null);
        }

        if ((mDataplanUse <= 0L) && (mSnapshotTime < 0)) {
            Log.d(TAG, "Display data usage from history");
            mDataplanUse = displayUsageLevel(usageLevel);
            mSnapshotTime = -1L;
        }

        summaryPreference.setUsageNumbers(mDataplanUse, mDataplanSize, mHasMobileData);

        if (mDataBarSize <= 0) {
            summaryPreference.setChartEnabled(false);
        } else {
            summaryPreference.setChartEnabled(true);
            summaryPreference.setLabels(DataUsageUtils.formatDataUsage(mContext, 0 /* sizeBytes */),
                    DataUsageUtils.formatDataUsage(mContext, mDataBarSize));
            summaryPreference.setProgress(mDataplanUse / (float) mDataBarSize);
        }
        summaryPreference.setUsageInfo(mCycleEnd, mSnapshotTime, mCarrierName, mDataplanCount);
    }

    private long displayUsageLevel(long usageLevel) {
        if (usageLevel > 0) {
            return usageLevel;
        }
        try {
            usageLevel = mHistoricalUsageLevel.get();
        } catch (Exception ex) {
        }
        return usageLevel;
    }

    // TODO(b/70950124) add test for this method once the robolectric shadow run script is
    // completed (b/3526807)
    private void refreshDataplanInfo(DataUsageController.DataUsageInfo info,
            SubscriptionInfo subInfo) {
        // reset data before overwriting
        mCarrierName = null;
        mDataplanCount = 0;
        mDataplanSize = -1L;
        mDataBarSize = mDataInfoController.getSummaryLimit(info);
        mDataplanUse = info.usageLevel;
        mCycleEnd = info.cycleEnd;
        mSnapshotTime = -1L;

        if (subInfo != null && mHasMobileData) {
            mCarrierName = subInfo.getCarrierName();
            final List<SubscriptionPlan> plans = getSubscriptionPlans(mSubId);
            final SubscriptionPlan primaryPlan = getPrimaryPlan(plans);

            if (primaryPlan != null) {
                mDataplanCount = plans.size();
                mDataplanSize = primaryPlan.getDataLimitBytes();
                if (unlimited(mDataplanSize)) {
                    mDataplanSize = -1L;
                }
                mDataBarSize = mDataplanSize;
                mDataplanUse = primaryPlan.getDataUsageBytes();

                RecurrenceRule rule = primaryPlan.getCycleRule();
                if (rule != null && rule.start != null && rule.end != null) {
                    mCycleEnd = rule.end.toEpochSecond() * 1000L;
                }
                mSnapshotTime = primaryPlan.getDataUsageTime();
            }
        }
        Log.i(TAG, "Have " + mDataplanCount + " plans, dflt sub-id " + mSubId);
    }

    private static SubscriptionPlan getPrimaryPlan(List<SubscriptionPlan> plans) {
        if (CollectionUtils.isEmpty(plans)) {
            return null;
        }
        // First plan in the list is the primary plan
        SubscriptionPlan plan = plans.get(0);
        return plan.getDataLimitBytes() > 0
                && validSize(plan.getDataUsageBytes())
                && plan.getCycleRule() != null ? plan : null;
    }

    private static boolean validSize(long value) {
        return value >= 0L && value < PETA;
    }

    public static boolean unlimited(long size) {
        return size == SubscriptionPlan.BYTES_UNLIMITED;
    }
}
Loading