1 /*
2  * Copyright (C) 2020 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.frameworks.core.batterystatsviewer;
18 
19 import android.content.Context;
20 import android.os.BatteryStatsManager;
21 import android.os.BatteryUsageStats;
22 import android.os.BatteryUsageStatsQuery;
23 import android.os.Bundle;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.widget.ImageView;
28 import android.widget.TextView;
29 import android.widget.Toast;
30 
31 import androidx.activity.ComponentActivity;
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.loader.app.LoaderManager;
35 import androidx.loader.app.LoaderManager.LoaderCallbacks;
36 import androidx.loader.content.Loader;
37 import androidx.recyclerview.widget.LinearLayoutManager;
38 import androidx.recyclerview.widget.RecyclerView;
39 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
40 
41 import com.android.settingslib.utils.AsyncLoaderCompat;
42 
43 import java.util.Collections;
44 import java.util.List;
45 import java.util.Locale;
46 
47 public class BatteryStatsViewerActivity extends ComponentActivity {
48     public static final String EXTRA_BATTERY_CONSUMER = "batteryConsumerId";
49 
50     private static final int BATTERY_STATS_REFRESH_RATE_MILLIS = 60 * 1000;
51     private static final int MILLIS_IN_MINUTE = 60000;
52     private static final String FORCE_FRESH_STATS = "force_fresh_stats";
53 
54     private BatteryStatsDataAdapter mBatteryStatsDataAdapter;
55     private final Runnable mBatteryStatsRefresh = this::refreshPeriodically;
56     private String mBatteryConsumerId;
57     private TextView mTitleView;
58     private TextView mDetailsView;
59     private ImageView mIconView;
60     private TextView mPackagesView;
61     private View mHeadingsView;
62     private RecyclerView mBatteryConsumerDataView;
63     private SwipeRefreshLayout mSwipeRefreshLayout;
64     private View mCardView;
65     private View mEmptyView;
66     private List<BatteryUsageStats> mBatteryUsageStats;
67 
68     @Override
onCreate(@ullable Bundle savedInstanceState)69     protected void onCreate(@Nullable Bundle savedInstanceState) {
70         super.onCreate(savedInstanceState);
71 
72         mBatteryConsumerId = getIntent().getStringExtra(EXTRA_BATTERY_CONSUMER);
73 
74         setContentView(R.layout.battery_stats_viewer_layout);
75 
76         mSwipeRefreshLayout = findViewById(R.id.swipe_refresh);
77         mSwipeRefreshLayout.setColorSchemeResources(android.R.color.holo_green_light);
78         mSwipeRefreshLayout.setRefreshing(true);
79         mSwipeRefreshLayout.setOnRefreshListener(this::onRefresh);
80 
81         mCardView = findViewById(R.id.app_card);
82         mTitleView = findViewById(android.R.id.title);
83         mDetailsView = findViewById(R.id.details);
84         mIconView = findViewById(android.R.id.icon);
85         mPackagesView = findViewById(R.id.packages);
86         mHeadingsView = findViewById(R.id.headings);
87 
88         mBatteryConsumerDataView = findViewById(R.id.battery_consumer_data_view);
89         mBatteryConsumerDataView.setLayoutManager(new LinearLayoutManager(this));
90         mBatteryStatsDataAdapter = new BatteryStatsDataAdapter();
91         mBatteryConsumerDataView.setAdapter(mBatteryStatsDataAdapter);
92 
93         mEmptyView = findViewById(R.id.empty_view);
94     }
95 
96     @Override
onResume()97     protected void onResume() {
98         super.onResume();
99         refreshPeriodically();
100     }
101 
102     @Override
onPause()103     protected void onPause() {
104         super.onPause();
105         getMainThreadHandler().removeCallbacks(mBatteryStatsRefresh);
106     }
107 
refreshPeriodically()108     private void refreshPeriodically() {
109         loadBatteryUsageStats(false);
110         getMainThreadHandler().postDelayed(mBatteryStatsRefresh, BATTERY_STATS_REFRESH_RATE_MILLIS);
111     }
112 
onRefresh()113     private void onRefresh() {
114         loadBatteryUsageStats(true);
115     }
116 
loadBatteryUsageStats(boolean forceFreshStats)117     private void loadBatteryUsageStats(boolean forceFreshStats) {
118         Bundle args = new Bundle();
119         args.putBoolean(FORCE_FRESH_STATS, forceFreshStats);
120         LoaderManager.getInstance(this).restartLoader(0, args,
121                 new BatteryUsageStatsLoaderCallbacks());
122     }
123 
124     private static class BatteryUsageStatsLoader extends
125             AsyncLoaderCompat<List<BatteryUsageStats>> {
126         private final BatteryStatsManager mBatteryStatsManager;
127         private final boolean mForceFreshStats;
128 
BatteryUsageStatsLoader(Context context, boolean forceFreshStats)129         BatteryUsageStatsLoader(Context context, boolean forceFreshStats) {
130             super(context);
131             mBatteryStatsManager = context.getSystemService(BatteryStatsManager.class);
132             mForceFreshStats = forceFreshStats;
133         }
134 
135         @Override
loadInBackground()136         public List<BatteryUsageStats> loadInBackground() {
137             final int maxStatsAgeMs = mForceFreshStats ? 0 : BATTERY_STATS_REFRESH_RATE_MILLIS;
138             final BatteryUsageStatsQuery queryDefault =
139                     new BatteryUsageStatsQuery.Builder()
140                             .includePowerModels()
141                             .includeProcessStateData()
142                             .setMaxStatsAgeMs(maxStatsAgeMs)
143                             .build();
144             final BatteryUsageStatsQuery queryPowerProfileModeledOnly =
145                     new BatteryUsageStatsQuery.Builder()
146                             .powerProfileModeledOnly()
147                             .includePowerModels()
148                             .includeProcessStateData()
149                             .setMaxStatsAgeMs(maxStatsAgeMs)
150                             .build();
151             return mBatteryStatsManager.getBatteryUsageStats(
152                     List.of(queryDefault, queryPowerProfileModeledOnly));
153         }
154 
155         @Override
onDiscardResult(List<BatteryUsageStats> result)156         protected void onDiscardResult(List<BatteryUsageStats> result) {
157         }
158     }
159 
160     private class BatteryUsageStatsLoaderCallbacks
161             implements LoaderCallbacks<List<BatteryUsageStats>> {
162         @NonNull
163         @Override
onCreateLoader(int id, Bundle args)164         public Loader<List<BatteryUsageStats>> onCreateLoader(int id, Bundle args) {
165             return new BatteryUsageStatsLoader(BatteryStatsViewerActivity.this,
166                     args.getBoolean(FORCE_FRESH_STATS));
167         }
168 
169         @Override
onLoadFinished(@onNull Loader<List<BatteryUsageStats>> loader, List<BatteryUsageStats> batteryUsageStats)170         public void onLoadFinished(@NonNull Loader<List<BatteryUsageStats>> loader,
171                 List<BatteryUsageStats> batteryUsageStats) {
172             onBatteryUsageStatsLoaded(batteryUsageStats);
173         }
174 
175         @Override
onLoaderReset(@onNull Loader<List<BatteryUsageStats>> loader)176         public void onLoaderReset(@NonNull Loader<List<BatteryUsageStats>> loader) {
177         }
178     }
179 
onBatteryUsageStatsLoaded(List<BatteryUsageStats> batteryUsageStats)180     private void onBatteryUsageStatsLoaded(List<BatteryUsageStats> batteryUsageStats) {
181         mBatteryUsageStats = batteryUsageStats;
182         onBatteryStatsDataLoaded();
183     }
184 
onBatteryStatsDataLoaded()185     public void onBatteryStatsDataLoaded() {
186         BatteryConsumerData batteryConsumerData = new BatteryConsumerData(this,
187                 mBatteryUsageStats, mBatteryConsumerId);
188 
189         BatteryConsumerInfoHelper.BatteryConsumerInfo
190                 batteryConsumerInfo = batteryConsumerData.getBatteryConsumerInfo();
191         if (batteryConsumerInfo == null) {
192             Toast.makeText(this, "Battery consumer not found: " + mBatteryConsumerId,
193                     Toast.LENGTH_SHORT).show();
194             finish();
195             return;
196         } else {
197             mTitleView.setText(batteryConsumerInfo.label);
198             if (batteryConsumerInfo.details != null) {
199                 mDetailsView.setText(batteryConsumerInfo.details);
200                 mDetailsView.setVisibility(View.VISIBLE);
201             } else {
202                 mDetailsView.setVisibility(View.GONE);
203             }
204             if (batteryConsumerInfo.iconInfo != null) {
205                 mIconView.setImageDrawable(
206                         batteryConsumerInfo.iconInfo.loadIcon(getPackageManager()));
207             } else {
208                 mIconView.setImageResource(R.drawable.gm_device_24);
209             }
210             if (batteryConsumerInfo.packages != null) {
211                 mPackagesView.setText(batteryConsumerInfo.packages);
212                 mPackagesView.setVisibility(View.VISIBLE);
213             } else {
214                 mPackagesView.setVisibility(View.GONE);
215             }
216 
217             if (batteryConsumerInfo.consumerType
218                     == BatteryConsumerData.ConsumerType.DEVICE_POWER_COMPONENT) {
219                 mHeadingsView.setVisibility(View.VISIBLE);
220             } else {
221                 mHeadingsView.setVisibility(View.GONE);
222             }
223         }
224 
225         mBatteryStatsDataAdapter.setEntries(batteryConsumerData.getEntries());
226         if (batteryConsumerData.getEntries().isEmpty()) {
227             mEmptyView.setVisibility(View.VISIBLE);
228             mBatteryConsumerDataView.setVisibility(View.GONE);
229         } else {
230             mEmptyView.setVisibility(View.GONE);
231             mBatteryConsumerDataView.setVisibility(View.VISIBLE);
232         }
233 
234         mCardView.setVisibility(View.VISIBLE);
235         mSwipeRefreshLayout.setRefreshing(false);
236     }
237 
238     private static class BatteryStatsDataAdapter extends
239             RecyclerView.Adapter<BatteryStatsDataAdapter.ViewHolder> {
240         public static class ViewHolder extends RecyclerView.ViewHolder {
241             public ImageView iconImageView;
242             public TextView titleTextView;
243             public TextView value1TextView;
244             public TextView value2TextView;
245 
ViewHolder(View itemView)246             ViewHolder(View itemView) {
247                 super(itemView);
248 
249                 iconImageView = itemView.findViewById(R.id.icon);
250                 titleTextView = itemView.findViewById(R.id.title);
251                 value1TextView = itemView.findViewById(R.id.value1);
252                 value2TextView = itemView.findViewById(R.id.value2);
253             }
254         }
255 
256         private List<BatteryConsumerData.Entry> mEntries = Collections.emptyList();
257 
setEntries(List<BatteryConsumerData.Entry> entries)258         public void setEntries(List<BatteryConsumerData.Entry> entries) {
259             mEntries = entries;
260             notifyDataSetChanged();
261         }
262 
263         @Override
getItemCount()264         public int getItemCount() {
265             return mEntries.size();
266         }
267 
268         @NonNull
269         @Override
onCreateViewHolder(@onNull ViewGroup parent, int position)270         public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) {
271             LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
272             View itemView = layoutInflater.inflate(R.layout.battery_consumer_entry_layout, parent,
273                     false);
274             return new ViewHolder(itemView);
275         }
276 
277         @Override
onBindViewHolder(@onNull ViewHolder viewHolder, int position)278         public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
279             BatteryConsumerData.Entry entry = mEntries.get(position);
280 
281             switch (entry.entryType) {
282                 case UID_TOTAL_POWER:
283                     setTitleIconAndBackground(viewHolder, entry.title,
284                             R.drawable.gm_sum_24, 0);
285                     setPowerText(viewHolder.value1TextView, entry.value1);
286                     setProportionText(viewHolder.value2TextView, entry);
287                     break;
288                 case UID_POWER_PROFILE:
289                     setTitleIconAndBackground(viewHolder, entry.title,
290                             R.drawable.gm_calculate_24,
291                             R.color.battery_consumer_bg_power_profile);
292                     setPowerText(viewHolder.value1TextView, entry.value1);
293                     setProportionText(viewHolder.value2TextView, entry);
294                     break;
295                 case UID_POWER_PROFILE_PROCESS_STATE:
296                     setTitleIconAndBackground(viewHolder, "    " + entry.title,
297                             R.drawable.gm_calculate_24,
298                             R.color.battery_consumer_bg_power_profile);
299                     setPowerText(viewHolder.value1TextView, entry.value1);
300                     viewHolder.value2TextView.setVisibility(View.INVISIBLE);
301                     break;
302                 case UID_POWER_ENERGY_CONSUMPTION:
303                     setTitleIconAndBackground(viewHolder, entry.title,
304                             R.drawable.gm_energy_24,
305                             R.color.battery_consumer_bg_energy_consumption);
306                     setPowerText(viewHolder.value1TextView, entry.value1);
307                     setProportionText(viewHolder.value2TextView, entry);
308                     break;
309                 case UID_POWER_ENERGY_PROCESS_STATE:
310                     setTitleIconAndBackground(viewHolder, "    " + entry.title,
311                             R.drawable.gm_energy_24,
312                             R.color.battery_consumer_bg_energy_consumption);
313                     setPowerText(viewHolder.value1TextView, entry.value1);
314                     viewHolder.value2TextView.setVisibility(View.INVISIBLE);
315                     break;
316                 case UID_POWER_CUSTOM:
317                     setTitleIconAndBackground(viewHolder, entry.title,
318                             R.drawable.gm_energy_24,
319                             R.color.battery_consumer_bg_energy_consumption);
320                     setPowerText(viewHolder.value1TextView, entry.value1);
321                     setProportionText(viewHolder.value2TextView, entry);
322                     break;
323                 case UID_DURATION:
324                     setTitleIconAndBackground(viewHolder, entry.title,
325                             R.drawable.gm_timer_24, 0);
326                     setDurationText(viewHolder.value1TextView, (long) entry.value1);
327                     setProportionText(viewHolder.value2TextView, entry);
328                     break;
329                 case DEVICE_TOTAL_POWER:
330                     setTitleIconAndBackground(viewHolder, entry.title,
331                             R.drawable.gm_sum_24, 0);
332                     setPowerText(viewHolder.value1TextView, entry.value1);
333                     setPowerText(viewHolder.value2TextView, entry.value2);
334                     break;
335                 case DEVICE_POWER_MODELED:
336                     setTitleIconAndBackground(viewHolder, entry.title,
337                             R.drawable.gm_calculate_24,
338                             R.color.battery_consumer_bg_power_profile);
339                     setPowerText(viewHolder.value1TextView, entry.value1);
340                     setPowerText(viewHolder.value2TextView, entry.value2);
341                     break;
342                 case DEVICE_POWER_ENERGY_CONSUMPTION:
343                     setTitleIconAndBackground(viewHolder, entry.title,
344                             R.drawable.gm_energy_24,
345                             R.color.battery_consumer_bg_energy_consumption);
346                     setPowerText(viewHolder.value1TextView, entry.value1);
347                     setPowerText(viewHolder.value2TextView, entry.value2);
348                     break;
349                 case DEVICE_POWER_CUSTOM:
350                     setTitleIconAndBackground(viewHolder, entry.title,
351                             R.drawable.gm_energy_24,
352                             R.color.battery_consumer_bg_energy_consumption);
353                     setPowerText(viewHolder.value1TextView, entry.value1);
354                     setPowerText(viewHolder.value2TextView, entry.value2);
355                     break;
356                 case DEVICE_DURATION:
357                     setTitleIconAndBackground(viewHolder, entry.title,
358                             R.drawable.gm_timer_24, 0);
359                     setDurationText(viewHolder.value1TextView, (long) entry.value1);
360                     viewHolder.value2TextView.setVisibility(View.GONE);
361                     break;
362             }
363         }
364 
setTitleIconAndBackground(ViewHolder viewHolder, String title, int icon, int background)365         private void setTitleIconAndBackground(ViewHolder viewHolder, String title, int icon,
366                 int background) {
367             viewHolder.titleTextView.setText(title);
368             viewHolder.iconImageView.setImageResource(icon);
369             viewHolder.itemView.setBackgroundResource(background);
370         }
371 
setProportionText(TextView textView, BatteryConsumerData.Entry entry)372         private void setProportionText(TextView textView, BatteryConsumerData.Entry entry) {
373             final double proportion = entry.value2 != 0 ? entry.value1 * 100 / entry.value2 : 0;
374             textView.setText(
375                     String.format(Locale.getDefault(), "%.1f%%", proportion));
376             textView.setVisibility(View.VISIBLE);
377         }
378 
setPowerText(TextView textView, double powerMah)379         private void setPowerText(TextView textView, double powerMah) {
380             textView.setText(String.format(Locale.getDefault(), "%.1f", powerMah));
381             textView.setVisibility(View.VISIBLE);
382         }
383 
setDurationText(TextView textView, long durationMs)384         private void setDurationText(TextView textView, long durationMs) {
385             CharSequence text;
386             if (durationMs < MILLIS_IN_MINUTE) {
387                 text = String.format(Locale.getDefault(), "%,d ms", durationMs);
388             } else {
389                 text = String.format(Locale.getDefault(), "%,d m %d s",
390                         durationMs / MILLIS_IN_MINUTE,
391                         (durationMs % MILLIS_IN_MINUTE) / 1000);
392             }
393 
394             textView.setText(text);
395             textView.setVisibility(View.VISIBLE);
396         }
397     }
398 }
399