diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8250b616b2..d213a5dab5 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -58,6 +58,7 @@
+
@@ -718,6 +719,8 @@
+
+
diff --git a/src/org/thoughtcrime/securesms/logsubmit/SubmitLogFragment.java b/src/org/thoughtcrime/securesms/logsubmit/SubmitLogFragment.java
index 6a972ed175..76a96c1586 100644
--- a/src/org/thoughtcrime/securesms/logsubmit/SubmitLogFragment.java
+++ b/src/org/thoughtcrime/securesms/logsubmit/SubmitLogFragment.java
@@ -21,6 +21,7 @@ import android.annotation.TargetApi;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
+import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -31,10 +32,6 @@ import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
import android.text.ClipboardManager;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
@@ -48,6 +45,12 @@ import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
import org.json.JSONException;
import org.json.JSONObject;
import org.thoughtcrime.securesms.ApplicationContext;
@@ -55,6 +58,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.logsubmit.util.Scrubber;
+import org.thoughtcrime.securesms.util.BucketInfo;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
@@ -67,6 +71,7 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
@@ -93,6 +98,7 @@ public class SubmitLogFragment extends Fragment {
private static final String HEADER_SYSINFO = "========== SYSINFO ========";
private static final String HEADER_JOBS = "=========== JOBS =========";
+ private static final String HEADER_POWER = "========== POWER =========";
private static final String HEADER_LOGCAT = "========== LOGCAT ========";
private static final String HEADER_LOGGER = "========== LOGGER ========";
@@ -371,14 +377,34 @@ public class SubmitLogFragment extends Fragment {
String scrubbedLogcat = scrubber.scrub(logcat);
Log.i(TAG, "Scrub logcat: " + (System.currentTimeMillis() - t4) + " ms");
- return HEADER_SYSINFO + "\n\n" +
- buildDescription(context) + "\n\n\n" +
- HEADER_JOBS + "\n\n" +
- scrubber.scrub(ApplicationContext.getInstance(context).getJobManager().getDebugInfo()) + "\n\n" +
- HEADER_LOGCAT + "\n\n" +
- scrubbedLogcat + "\n\n\n" +
- HEADER_LOGGER + "\n\n" +
- newLogs;
+
+ StringBuilder stringBuilder = new StringBuilder();
+
+ stringBuilder.append(HEADER_SYSINFO)
+ .append("\n\n")
+ .append(buildDescription(context))
+ .append("\n\n\n")
+ .append(HEADER_JOBS)
+ .append("\n\n")
+ .append(scrubber.scrub(ApplicationContext.getInstance(context).getJobManager().getDebugInfo()))
+ .append("\n\n\n");
+
+ if (VERSION.SDK_INT >= 22) {
+ stringBuilder.append(HEADER_POWER)
+ .append("\n\n")
+ .append(buildPower(context))
+ .append("\n\n\n");
+ }
+
+ stringBuilder.append(HEADER_LOGCAT)
+ .append("\n\n")
+ .append(scrubbedLogcat)
+ .append("\n\n\n")
+ .append(HEADER_LOGGER)
+ .append("\n\n")
+ .append(newLogs);
+
+ return stringBuilder.toString();
}
@Override
@@ -485,7 +511,7 @@ public class SubmitLogFragment extends Fragment {
return activityManager.getMemoryClass() + lowMem;
}
- private static String buildDescription(Context context) {
+ private static CharSequence buildDescription(Context context) {
final PackageManager pm = context.getPackageManager();
final StringBuilder builder = new StringBuilder();
@@ -513,7 +539,23 @@ public class SubmitLogFragment extends Fragment {
builder.append("Unknown\n");
}
- return builder.toString();
+ return builder;
+ }
+
+ @RequiresApi(api = 22)
+ private static CharSequence buildPower(@NonNull Context context) {
+ final UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
+
+ if (usageStatsManager == null) {
+ return "UsageStatsManager not available";
+ }
+
+ BucketInfo info = BucketInfo.getInfo(usageStatsManager, TimeUnit.DAYS.toMillis(3));
+
+ return new StringBuilder().append("Current bucket: ").append(BucketInfo.bucketToString(info.getCurrentBucket())).append('\n')
+ .append("Highest bucket: ").append(BucketInfo.bucketToString(info.getBestBucket())).append('\n')
+ .append("Lowest bucket : ").append(BucketInfo.bucketToString(info.getWorstBucket())).append("\n\n")
+ .append(info.getHistory());
}
private static Iterable getSupportedAbis() {
diff --git a/src/org/thoughtcrime/securesms/util/BucketInfo.java b/src/org/thoughtcrime/securesms/util/BucketInfo.java
new file mode 100644
index 0000000000..58a3181713
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/util/BucketInfo.java
@@ -0,0 +1,95 @@
+package org.thoughtcrime.securesms.util;
+
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.util.Date;
+
+@RequiresApi(api = 22)
+public final class BucketInfo {
+
+ /**
+ * UsageStatsManager.STANDBY_BUCKET_EXEMPTED: is a Hidden API
+ */
+ public static final int STANDBY_BUCKET_EXEMPTED = 5;
+
+ private final int currentBucket;
+ private final int worstBucket;
+ private final int bestBucket;
+ private final CharSequence history;
+
+ private BucketInfo(int currentBucket, int worstBucket, int bestBucket, CharSequence history) {
+ this.currentBucket = currentBucket;
+ this.worstBucket = worstBucket;
+ this.bestBucket = bestBucket;
+ this.history = history;
+ }
+
+ public static @NonNull BucketInfo getInfo(@NonNull UsageStatsManager usageStatsManager, long overLastDurationMs) {
+ StringBuilder stringBuilder = new StringBuilder();
+
+ int currentBucket = usageStatsManager.getAppStandbyBucket();
+ int worseBucket = currentBucket;
+ int bestBucket = currentBucket;
+
+ long now = System.currentTimeMillis();
+ UsageEvents.Event event = new UsageEvents.Event();
+ UsageEvents usageEvents = usageStatsManager.queryEventsForSelf(now - overLastDurationMs, now);
+
+ while (usageEvents.hasNextEvent()) {
+ usageEvents.getNextEvent(event);
+
+ if (event.getEventType() == UsageEvents.Event.STANDBY_BUCKET_CHANGED) {
+ int appStandbyBucket = event.getAppStandbyBucket();
+
+ stringBuilder.append(new Date(event.getTimeStamp()))
+ .append(": ")
+ .append("Bucket Change: ")
+ .append(bucketToString(appStandbyBucket))
+ .append("\n");
+
+ if (appStandbyBucket > worseBucket) {
+ worseBucket = appStandbyBucket;
+ }
+ if (appStandbyBucket < bestBucket) {
+ bestBucket = appStandbyBucket;
+ }
+ }
+ }
+
+ return new BucketInfo(currentBucket, worseBucket, bestBucket, stringBuilder);
+ }
+
+ /**
+ * Not localized, for logs and debug only.
+ */
+ public static String bucketToString(int bucket) {
+ switch (bucket) {
+ case UsageStatsManager.STANDBY_BUCKET_ACTIVE: return "Active";
+ case UsageStatsManager.STANDBY_BUCKET_FREQUENT: return "Frequent";
+ case UsageStatsManager.STANDBY_BUCKET_WORKING_SET: return "Working Set";
+ case UsageStatsManager.STANDBY_BUCKET_RARE: return "Rare";
+ case STANDBY_BUCKET_EXEMPTED: return "Exempted";
+ default: return "Unknown " + bucket;
+ }
+ }
+
+ public int getBestBucket() {
+ return bestBucket;
+ }
+
+ public int getWorstBucket() {
+ return worstBucket;
+ }
+
+ public int getCurrentBucket() {
+ return currentBucket;
+ }
+
+ public CharSequence getHistory() {
+ return history;
+ }
+}