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

Commit b350c66d authored by ykhung's avatar ykhung
Browse files

Move the battery components from SettingsGoogle to Settings

Bug: 183921876
Test: make SettingsRoboTests
Test: make SettingsGoogleRoboTests
Change-Id: I47cbba1cbfd6cb17745e7aaaf56b22e3c9dcd30e
parent 0a03f8e2
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project
<!-- Copyright (C) 2021 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.
+268 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.fuelgauge;

import static java.lang.Math.round;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;

import androidx.appcompat.widget.AppCompatImageView;

import com.android.settings.R;
import com.android.settingslib.Utils;

import java.util.Locale;

/** A widget component to draw chart graph. */
public class BatteryChartView extends AppCompatImageView implements View.OnClickListener {
    private static final String TAG = "BatteryChartView";
    private static final int DEFAULT_TRAPEZOID_COUNT = 12;
    /** Selects all trapezoid shapes. */
    public static final int SELECTED_INDEX_ALL = -1;
    public static final int SELECTED_INDEX_INVALID = -2;

    /** A callback listener for selected group index is updated. */
    public interface OnSelectListener {
        void onSelect(int trapezoidIndex);
    }

    private int mDividerWidth;
    private int mDividerHeight;
    private int mTrapezoidCount;
    private int mSelectedIndex;
    private float mTrapezoidVOffset;
    private float mTrapezoidHOffset;
    // Colors for drawing the trapezoid shape and dividers.
    private int mTrapezoidColor;
    private int mTrapezoidSolidColor;
    private final int mDividerColor = Color.parseColor("#CDCCC5");

    private int[] mLevels;
    private Paint mDividerPaint;
    private Paint mTrapezoidPaint;
    private TrapezoidSlot[] mTrapezoidSlot;
    // Records the location to calculate selected index.
    private MotionEvent mTouchUpEvent;
    private BatteryChartView.OnSelectListener mOnSelectListener;

    public BatteryChartView(Context context) {
        super(context, null);
    }

    public BatteryChartView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initializeColors(context);
        setOnClickListener(this);
        setSelectedIndex(SELECTED_INDEX_ALL);
        setTrapezoidCount(DEFAULT_TRAPEZOID_COUNT);
    }

    /** Sets the total trapezoid count for drawing. */
    public void setTrapezoidCount(int trapezoidCount) {
        Log.i(TAG, "trapezoidCount:" + trapezoidCount);
        mTrapezoidCount = trapezoidCount;
        mTrapezoidSlot = new TrapezoidSlot[trapezoidCount];
        // Allocates the trapezoid slot array.
        for (int index = 0; index < trapezoidCount; index++) {
            mTrapezoidSlot[index] = new TrapezoidSlot();
        }
        invalidate();
    }

    /** Sets all levels value to draw the trapezoid shape */
    public void setLevels(int[] levels) {
        // We should provide trapezoid count + 1 data to draw all trapezoids.
        mLevels = levels.length == mTrapezoidCount + 1 ? levels : null;
        invalidate();
    }

    /** Sets the selected group index to draw highlight effect. */
    public void setSelectedIndex(int index) {
        if (mSelectedIndex != index) {
            mSelectedIndex = index;
            invalidate();
            // Callbacks to the listener if we have.
            if (mOnSelectListener != null) {
                mOnSelectListener.onSelect(mSelectedIndex);
            }
        }
    }

    /** Sets the callback to monitor the selected group index. */
    public void setOnSelectListener(BatteryChartView.OnSelectListener listener) {
        mOnSelectListener = listener;
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        drawHorizontalDividers(canvas);
        drawVerticalDividers(canvas);
        drawTrapezoids(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Caches the location to calculate selected trapezoid index.
        final int action = event.getAction();
        if (action == MotionEvent.ACTION_UP) {
            mTouchUpEvent = MotionEvent.obtain(event);
        } else if (action == MotionEvent.ACTION_CANCEL) {
            mTouchUpEvent = null; // reset
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void onClick(View view) {
        if (mTouchUpEvent == null) {
            Log.w(TAG, "invalid motion event for onClick() callback");
            return;
        }
        final int trapezoidIndex = getTrapezoidIndex(mTouchUpEvent.getX());
        // Selects all if users click the same trapezoid item two times.
        if (trapezoidIndex == mSelectedIndex) {
            setSelectedIndex(SELECTED_INDEX_ALL);
        } else {
            setSelectedIndex(trapezoidIndex);
        }
        view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
    }

    private void initializeColors(Context context) {
        setBackgroundColor(Color.TRANSPARENT);
        mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
        mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor);
        // Initializes the divider line paint.
        final Resources resources = getContext().getResources();
        mDividerWidth = resources.getDimensionPixelSize(R.dimen.chartview_divider_width);
        mDividerHeight = resources.getDimensionPixelSize(R.dimen.chartview_divider_height);
        mDividerPaint = new Paint();
        mDividerPaint.setAntiAlias(true);
        mDividerPaint.setColor(mDividerColor);
        mDividerPaint.setStyle(Paint.Style.STROKE);
        mDividerPaint.setStrokeWidth(mDividerWidth);
        Log.i(TAG, "mDividerWidth:" + mDividerWidth);
        Log.i(TAG, "mDividerHeight:" + mDividerHeight);
        // Initializes the trapezoid paint.
        mTrapezoidHOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_start);
        mTrapezoidVOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_bottom);
        mTrapezoidPaint = new Paint();
        mTrapezoidPaint.setAntiAlias(true);
        mTrapezoidPaint.setColor(mTrapezoidSolidColor);
        mTrapezoidPaint.setStyle(Paint.Style.FILL);
        mTrapezoidPaint.setPathEffect(
            new CornerPathEffect(
                resources.getDimensionPixelSize(R.dimen.chartview_trapezoid_radius)));
    }

    private void drawHorizontalDividers(Canvas canvas) {
        // Draws the top divider line for 100% curve.
        float offsetY = mDividerWidth * 0.5f;
        canvas.drawLine(0, offsetY, getWidth(), offsetY, mDividerPaint);
        // Draws the center divider line for 50% curve.
        final float availableSpace =
                getHeight() - mDividerWidth * 2 - mTrapezoidVOffset - mDividerHeight;
        offsetY = mDividerWidth + availableSpace * 0.5f;
        canvas.drawLine(0, offsetY, getWidth(), offsetY, mDividerPaint);
        // Draws the center divider line for 0% curve.
        offsetY = getHeight() - mDividerHeight - mDividerWidth * 0.5f;
        canvas.drawLine(0, offsetY, getWidth(), offsetY, mDividerPaint);
    }

    private void drawVerticalDividers(Canvas canvas) {
        final int dividerCount = mTrapezoidCount + 1;
        final float dividerSpace = dividerCount * mDividerWidth;
        final float unitWidth = (getWidth() - dividerSpace) / (float) mTrapezoidCount;
        final float startY = getHeight() - mDividerHeight;
        final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * 0.5f;
        // Draws each vertical dividers.
        float startX = mDividerWidth * 0.5f;
        for (int index = 0; index < dividerCount; index++) {
            canvas.drawLine(startX, startY, startX, getHeight(), mDividerPaint);
            final float nextX = startX + mDividerWidth + unitWidth;
            // Updates the trapezoid slots for drawing.
            if (index < mTrapezoidSlot.length) {
                mTrapezoidSlot[index].mLeft = round(startX + trapezoidSlotOffset);
                mTrapezoidSlot[index].mRight = round(nextX - trapezoidSlotOffset);
            }
            startX = nextX;
        }
    }

    private void drawTrapezoids(Canvas canvas) {
        // Ignores invalid trapezoid data.
        if (mLevels == null) {
            return;
        }
        final float trapezoidBottom =
            getHeight() - mDividerHeight - mDividerWidth - mTrapezoidVOffset;
        final float availableSpace = trapezoidBottom - mDividerWidth;
        final float unitHeight = availableSpace / 100f;
        // Draws all trapezoid shapes into the canvas.
        final Path trapezoidPath = new Path();
        for (int index = 0; index < mTrapezoidCount; index++) {
            // Configures the trapezoid paint color.
            mTrapezoidPaint.setColor(
                mSelectedIndex == index || mSelectedIndex == SELECTED_INDEX_ALL
                ? mTrapezoidSolidColor
                : mTrapezoidColor);
            final float leftTop = round(trapezoidBottom - mLevels[index] * unitHeight);
            final float rightTop = round(trapezoidBottom - mLevels[index + 1] * unitHeight);
            trapezoidPath.reset();
            trapezoidPath.moveTo(mTrapezoidSlot[index].mLeft, trapezoidBottom);
            trapezoidPath.lineTo(mTrapezoidSlot[index].mLeft, leftTop);
            trapezoidPath.lineTo(mTrapezoidSlot[index].mRight, rightTop);
            trapezoidPath.lineTo(mTrapezoidSlot[index].mRight, trapezoidBottom);
            // A tricky way to make the trapezoid shape drawing the rounded corner.
            trapezoidPath.lineTo(mTrapezoidSlot[index].mLeft, trapezoidBottom);
            trapezoidPath.lineTo(mTrapezoidSlot[index].mLeft, leftTop);
            // Draws the trapezoid shape into canvas.
            canvas.drawPath(trapezoidPath, mTrapezoidPaint);
        }
    }

    // Searches the corresponding trapezoid index from x location.
    private int getTrapezoidIndex(float x) {
        for (int index = 0; index < mTrapezoidSlot.length; index++) {
            final TrapezoidSlot slot = mTrapezoidSlot[index];
            if (x >= slot.mLeft && x <= slot.mRight) {
                return index;
            }
        }
        return SELECTED_INDEX_INVALID;
    }

    // A container class for each trapezoid left and right location.
    private static final class TrapezoidSlot {
        public float mLeft;
        public float mRight;

        @Override
        public String toString() {
            return String.format(Locale.US, "TrapezoidSlot[%f,%f]", mLeft, mRight);
        }
    }
}
+112 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.fuelgauge;

import android.content.ContentValues;

/** A container class to carry data from {@link ContentValues}. */
public final class BatteryHistEntry {
    private static final String TAG = "BatteryHistEntry";

    public final long mUid;
    public final long mUserId;
    public final String mAppLabel;
    public final String mPackageName;
    // Whether the data is represented as system component or not?
    public final boolean mIsHidden;
    // Records the timestamp relative information.
    public final long mTimestamp;
    public final String mZoneId;
    // Records the battery usage relative information.
    public final double mTotalPower;
    public final double mConsumePower;
    public final double mPercentOfTotal;
    public final long mForegroundUsageTimeInMs;
    public final long mBackgroundUsageTimeInMs;
    public final int mDrainType;
    public final int mConsumerType;
    // Records the battery intent relative information.
    public final int mBatteryLevel;
    public final int mBatteryStatus;
    public final int mBatteryHealth;

    private boolean mIsValidEntry = true;
    private ContentValues mContentValues;

    public BatteryHistEntry(ContentValues contentValues) {
        mContentValues = contentValues;
        mUid = getLong("uid");
        mUserId = getLong("userId");
        mAppLabel = getString("appLabel");
        mPackageName = getString("packageName");
        mIsHidden = getBoolean("isHidden");
        mTimestamp = getLong("timestamp");
        mZoneId = getString("zoneId");
        mTotalPower = getDouble("totalPower");
        mConsumePower = getDouble("consumePower");
        mPercentOfTotal = getDouble("percentOfTotal");
        mForegroundUsageTimeInMs = getLong("foregroundUsageTimeInMs");
        mBackgroundUsageTimeInMs = getLong("backgroundUsageTimeInMs");
        mDrainType = getInteger("drainType");
        mConsumerType = getInteger("consumerType");
        mBatteryLevel = getInteger("batteryLevel");
        mBatteryStatus = getInteger("batteryStatus");
        mBatteryHealth = getInteger("batteryHealth");
    }

    /** Whether this {@link BatteryHistEntry} is valid or not? */
    public boolean isValidEntry() {
        return mIsValidEntry;
    }

    private int getInteger(String key) {
        if (mContentValues != null && mContentValues.containsKey(key)) {
            return mContentValues.getAsInteger(key);
        };
        mIsValidEntry = false;
        return -1;
    }

    private long getLong(String key) {
        if (mContentValues != null && mContentValues.containsKey(key)) {
            return mContentValues.getAsLong(key);
        }
        mIsValidEntry = false;
        return -1L;
    }

    private double getDouble(String key) {
        if (mContentValues != null && mContentValues.containsKey(key)) {
            return mContentValues.getAsDouble(key);
        }
        mIsValidEntry = false;
        return 0f;
    }

    private String getString(String key) {
        if (mContentValues != null && mContentValues.containsKey(key)) {
            return mContentValues.getAsString(key);
        }
        mIsValidEntry = false;
        return null;
    }

    private boolean getBoolean(String key) {
        if (mContentValues != null && mContentValues.containsKey(key)) {
            return mContentValues.getAsBoolean(key);
        }
        mIsValidEntry = false;
        return false;
    }
}
+105 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.fuelgauge;

import android.annotation.IntDef;
import android.content.ContentValues;
import android.os.BatteryConsumer;
import android.os.BatteryUsageStats;
import android.os.SystemBatteryConsumer;
import android.os.UidBatteryConsumer;
import android.os.UserBatteryConsumer;
import android.os.UserHandle;
import android.util.Log;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.TimeZone;

/** A utility class to convert data into another types. */
public final class ConvertUtils {
    private static final String TAG = "ConvertUtils";
    /** Invalid system battery consumer drain type. */
    public static final int INVALID_DRAIN_TYPE = -1;

    @IntDef(prefix = {"CONSUMER_TYPE"}, value = {
        CONSUMER_TYPE_UNKNOWN,
        CONSUMER_TYPE_UID_BATTERY,
        CONSUMER_TYPE_USER_BATTERY,
        CONSUMER_TYPE_SYSTEM_BATTERY,
    })
    @Retention(RetentionPolicy.SOURCE)
    public static @interface ConsumerType {}

    public static final int CONSUMER_TYPE_UNKNOWN = 0;
    public static final int CONSUMER_TYPE_UID_BATTERY = 1;
    public static final int CONSUMER_TYPE_USER_BATTERY = 2;
    public static final int CONSUMER_TYPE_SYSTEM_BATTERY = 3;

    /** Gets consumer type from {@link BatteryConsumer}. */
    @ConsumerType
    public static int getConsumerType(BatteryConsumer consumer) {
        if (consumer instanceof UidBatteryConsumer) {
            return CONSUMER_TYPE_UID_BATTERY;
        } else if (consumer instanceof UserBatteryConsumer) {
            return CONSUMER_TYPE_USER_BATTERY;
        } else if (consumer instanceof SystemBatteryConsumer) {
            return CONSUMER_TYPE_SYSTEM_BATTERY;
        } else {
          return CONSUMER_TYPE_UNKNOWN;
        }
    }

    /** Gets battery drain type for {@link SystemBatteryConsumer}. */
    public static int getDrainType(BatteryConsumer consumer) {
        if (consumer instanceof SystemBatteryConsumer) {
            return ((SystemBatteryConsumer) consumer).getDrainType();
        }
        return INVALID_DRAIN_TYPE;
    }

    public static ContentValues convert(
            BatteryEntry entry,
            BatteryUsageStats batteryUsageStats,
            int batteryLevel,
            int batteryStatus,
            int batteryHealth,
            long timestamp) {
        final ContentValues values = new ContentValues();
        values.put("uid", Long.valueOf(entry.getUid()));
        values.put("userId",
            Long.valueOf(UserHandle.getUserId(entry.getUid())));
        values.put("appLabel", entry.getLabel());
        values.put("packageName", entry.getDefaultPackageName());
        values.put("isHidden", Boolean.valueOf(entry.isHidden()));
        values.put("timestamp", Long.valueOf(timestamp));
        values.put("zoneId", TimeZone.getDefault().getID());
        values.put("totalPower",
            Double.valueOf(batteryUsageStats.getConsumedPower()));
        values.put("consumePower", Double.valueOf(entry.getConsumedPower()));
        values.put("percentOfTotal", Double.valueOf(entry.percent));
        values.put("foregroundUsageTimeInMs",
            Long.valueOf(entry.getTimeInForegroundMs()));
        values.put("backgroundUsageTimeInMs",
            Long.valueOf(entry.getTimeInBackgroundMs()));
        values.put("drainType", getDrainType(entry.getBatteryConsumer()));
        values.put("consumerType", getConsumerType(entry.getBatteryConsumer()));
        values.put("batteryLevel", Integer.valueOf(batteryLevel));
        values.put("batteryStatus", Integer.valueOf(batteryStatus));
        values.put("batteryHealth", Integer.valueOf(batteryHealth));
        return values;
    }

    private ConvertUtils() {}
}
+110 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.fuelgauge;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;

import android.content.ContentValues;
import android.os.BatteryConsumer;
import android.os.BatteryManager;
import android.os.BatteryUsageStats;
import android.os.SystemBatteryConsumer;
import android.os.UserHandle;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;

import java.util.TimeZone;

@RunWith(RobolectricTestRunner.class)
public final class BatteryHistEntryTest {

    @Mock
    private BatteryEntry mockBatteryEntry;
    @Mock
    private BatteryUsageStats mBatteryUsageStats;
    @Mock
    private SystemBatteryConsumer mockSystemBatteryConsumer;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testConstructor_returnsExpectedResult() {
        final int expectedType = 3;
        when(mockBatteryEntry.getUid()).thenReturn(1001);
        when(mockBatteryEntry.getLabel()).thenReturn("Settings");
        when(mockBatteryEntry.getDefaultPackageName())
            .thenReturn("com.google.android.settings.battery");
        when(mockBatteryEntry.isHidden()).thenReturn(true);
        when(mBatteryUsageStats.getConsumedPower()).thenReturn(5.1);
        when(mockBatteryEntry.getConsumedPower()).thenReturn(1.1);
        mockBatteryEntry.percent = 0.3;
        when(mockBatteryEntry.getTimeInForegroundMs()).thenReturn(1234L);
        when(mockBatteryEntry.getTimeInBackgroundMs()).thenReturn(5689L);
        when(mockBatteryEntry.getBatteryConsumer())
            .thenReturn(mockSystemBatteryConsumer);
        when(mockSystemBatteryConsumer.getDrainType()).thenReturn(expectedType);
        final ContentValues values =
            ConvertUtils.convert(
                mockBatteryEntry,
                mBatteryUsageStats,
                /*batteryLevel=*/ 12,
                /*batteryStatus=*/ BatteryManager.BATTERY_STATUS_FULL,
                /*batteryHealth=*/ BatteryManager.BATTERY_HEALTH_COLD,
                /*timestamp=*/ 10001L);

        final BatteryHistEntry entry = new BatteryHistEntry(values);

        assertThat(entry.isValidEntry()).isTrue();
        assertThat(entry.mUid).isEqualTo(1001);
        assertThat(entry.mUserId).isEqualTo(UserHandle.getUserId(1001));
        assertThat(entry.mAppLabel).isEqualTo("Settings");
        assertThat(entry.mPackageName)
            .isEqualTo("com.google.android.settings.battery");
        assertThat(entry.mIsHidden).isTrue();
        assertThat(entry.mTimestamp).isEqualTo(10001L);
        assertThat(entry.mZoneId).isEqualTo(TimeZone.getDefault().getID());
        assertThat(entry.mTotalPower).isEqualTo(5.1);
        assertThat(entry.mConsumePower).isEqualTo(1.1);
        assertThat(entry.mPercentOfTotal).isEqualTo(mockBatteryEntry.percent);
        assertThat(entry.mForegroundUsageTimeInMs).isEqualTo(1234L);
        assertThat(entry.mBackgroundUsageTimeInMs).isEqualTo(5689L);
        assertThat(entry.mDrainType).isEqualTo(expectedType);
        assertThat(entry.mConsumerType)
            .isEqualTo(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY);
        assertThat(entry.mBatteryLevel).isEqualTo(12);
        assertThat(entry.mBatteryStatus)
            .isEqualTo(BatteryManager.BATTERY_STATUS_FULL);
        assertThat(entry.mBatteryHealth)
            .isEqualTo(BatteryManager.BATTERY_HEALTH_COLD);
    }

    @Test
    public void testConstructor_invalidField_returnsInvalidEntry() {
        final BatteryHistEntry entry = new BatteryHistEntry(new ContentValues());
        assertThat(entry.isValidEntry()).isFalse();
    }
}
Loading