Add statusbar test harness app
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "se.ajpanton.statusbartweak.harness"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "se.ajpanton.statusbartweak.harness"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.microphone"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@drawable/ic_harness"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.StatusBarTweakHarness">
|
||||
<service
|
||||
android:name=".HarnessForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync|mediaPlayback" />
|
||||
<activity
|
||||
android:name=".ScenarioActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package se.ajpanton.statusbartweak.harness;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.media.MediaMetadata;
|
||||
import android.media.session.MediaSession;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
public final class HarnessForegroundService extends Service {
|
||||
public static final String ACTION_START_MEDIA = "se.ajpanton.statusbartweak.harness.START_MEDIA";
|
||||
public static final String ACTION_START_FOREGROUND =
|
||||
"se.ajpanton.statusbartweak.harness.START_FOREGROUND";
|
||||
public static final String ACTION_STOP = "se.ajpanton.statusbartweak.harness.STOP";
|
||||
|
||||
private static final String CHANNEL_ID = "sbt_harness_foreground";
|
||||
private static final int NOTIFICATION_ID = 42_000;
|
||||
|
||||
private MediaSession mediaSession;
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
String action = intent != null ? intent.getAction() : null;
|
||||
if (ACTION_STOP.equals(action)) {
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
if (ACTION_START_MEDIA.equals(action)) {
|
||||
startMediaState();
|
||||
return START_STICKY;
|
||||
}
|
||||
startForegroundState();
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (mediaSession != null) {
|
||||
mediaSession.setActive(false);
|
||||
mediaSession.release();
|
||||
mediaSession = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void startMediaState() {
|
||||
ensureChannel();
|
||||
if (mediaSession == null) {
|
||||
mediaSession = new MediaSession(this, "StatusBarTweakHarness");
|
||||
}
|
||||
mediaSession.setMetadata(new MediaMetadata.Builder()
|
||||
.putString(MediaMetadata.METADATA_KEY_TITLE, "Harness media")
|
||||
.putString(MediaMetadata.METADATA_KEY_ARTIST, "StatusBarTweak")
|
||||
.build());
|
||||
mediaSession.setPlaybackState(new PlaybackState.Builder()
|
||||
.setState(PlaybackState.STATE_PLAYING, 0, 1f)
|
||||
.setActions(PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_STOP)
|
||||
.build());
|
||||
mediaSession.setActive(true);
|
||||
|
||||
Notification.Builder builder = baseBuilder("Harness media", "MediaSession playback active")
|
||||
.setCategory(Notification.CATEGORY_TRANSPORT)
|
||||
.setOngoing(true)
|
||||
.setStyle(new Notification.MediaStyle()
|
||||
.setMediaSession(mediaSession.getSessionToken())
|
||||
.setShowActionsInCompactView(0))
|
||||
.addAction(R.drawable.ic_notify, "Stop", stopIntent());
|
||||
startForegroundCompat(builder.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||
}
|
||||
|
||||
private void startForegroundState() {
|
||||
ensureChannel();
|
||||
Notification notification = baseBuilder(
|
||||
"Harness foreground service",
|
||||
"Generic foreground service notification active")
|
||||
.setCategory(Notification.CATEGORY_SERVICE)
|
||||
.setOngoing(true)
|
||||
.addAction(R.drawable.ic_notify, "Stop", stopIntent())
|
||||
.build();
|
||||
startForegroundCompat(notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
||||
}
|
||||
|
||||
private Notification.Builder baseBuilder(String title, String text) {
|
||||
Intent launch = new Intent(this, MainActivity.class);
|
||||
PendingIntent launchIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
1,
|
||||
launch,
|
||||
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
return new Notification.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setContentIntent(launchIntent)
|
||||
.setShowWhen(true);
|
||||
}
|
||||
|
||||
private PendingIntent stopIntent() {
|
||||
Intent stop = new Intent(this, HarnessForegroundService.class);
|
||||
stop.setAction(ACTION_STOP);
|
||||
return PendingIntent.getService(
|
||||
this,
|
||||
2,
|
||||
stop,
|
||||
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private void startForegroundCompat(Notification notification, int type) {
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
startForeground(NOTIFICATION_ID, notification, type);
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm == null) {
|
||||
return;
|
||||
}
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"StatusBarTweak harness foreground",
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel.setDescription("Foreground and media scenarios for StatusBarTweak testing");
|
||||
channel.setShowBadge(false);
|
||||
nm.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
package se.ajpanton.statusbartweak.harness;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
public final class MainActivity extends Activity {
|
||||
public static final String EXTRA_COMMAND = "command";
|
||||
public static final String COMMAND_POST_NORMAL = "post_normal";
|
||||
public static final String COMMAND_POST_ONGOING = "post_ongoing";
|
||||
public static final String COMMAND_POST_GROUPED = "post_grouped";
|
||||
public static final String COMMAND_UPDATE = "update";
|
||||
public static final String COMMAND_START_CHRONOMETER = "start_chronometer";
|
||||
public static final String COMMAND_START_CHURN = "start_churn";
|
||||
public static final String COMMAND_STOP_CHURN = "stop_churn";
|
||||
public static final String COMMAND_START_MEDIA = "start_media";
|
||||
public static final String COMMAND_START_FOREGROUND = "start_foreground";
|
||||
public static final String COMMAND_STOP_SERVICE = "stop_service";
|
||||
public static final String COMMAND_CLEAR = "clear";
|
||||
|
||||
private static final String CHANNEL_ID = "sbt_harness";
|
||||
private static final int NOTIFICATION_BASE_ID = 41_000;
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private int updatingCounter = 0;
|
||||
private int churnCounter = 0;
|
||||
private Runnable churnRunnable;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestNotificationPermissionIfNeeded();
|
||||
setContentView(createContent());
|
||||
handleCommand(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
handleCommand(intent);
|
||||
}
|
||||
|
||||
private View createContent() {
|
||||
ScrollView scroll = new ScrollView(this);
|
||||
scroll.setBackgroundColor(0xfff7efe5);
|
||||
LinearLayout root = new LinearLayout(this);
|
||||
root.setOrientation(LinearLayout.VERTICAL);
|
||||
root.setPadding(dp(20), dp(20), dp(20), dp(28));
|
||||
scroll.addView(root, new ScrollView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
TextView title = new TextView(this);
|
||||
title.setText("StatusBarTweak Test Harness");
|
||||
title.setTextColor(Color.BLACK);
|
||||
title.setTextSize(24);
|
||||
title.setGravity(Gravity.START);
|
||||
root.addView(title, matchWrap());
|
||||
|
||||
TextView description = new TextView(this);
|
||||
description.setText(
|
||||
"Open a scenario, then observe StatusBarTweak behavior. "
|
||||
+ "The same APK can be used on emulators and physical devices.");
|
||||
description.setTextColor(0xff333333);
|
||||
description.setTextSize(15);
|
||||
description.setPadding(0, dp(8), 0, dp(12));
|
||||
root.addView(description, matchWrap());
|
||||
|
||||
addSection(root, "Statusbar surfaces");
|
||||
addScenario(root, "Opaque light statusbar", ScenarioActivity.SCENARIO_OPAQUE_LIGHT);
|
||||
addScenario(root, "Opaque dark statusbar", ScenarioActivity.SCENARIO_OPAQUE_DARK);
|
||||
addScenario(root, "Transparent statusbar", ScenarioActivity.SCENARIO_TRANSPARENT);
|
||||
addScenario(root, "Fullscreen portrait", ScenarioActivity.SCENARIO_FULLSCREEN_PORTRAIT);
|
||||
addScenario(root, "Fullscreen landscape", ScenarioActivity.SCENARIO_FULLSCREEN_LANDSCAPE);
|
||||
|
||||
addSection(root, "Privacy indicators");
|
||||
addScenario(root, "Camera privacy indicator", ScenarioActivity.SCENARIO_CAMERA);
|
||||
addScenario(root, "Microphone privacy indicator", ScenarioActivity.SCENARIO_MICROPHONE);
|
||||
addScenario(root, "Camera + microphone indicators", ScenarioActivity.SCENARIO_CAMERA_MICROPHONE);
|
||||
|
||||
addSection(root, "Chip-like public states");
|
||||
addButton(root, "Start media session", v -> startHarnessService(
|
||||
HarnessForegroundService.ACTION_START_MEDIA));
|
||||
addButton(root, "Start foreground service", v -> startHarnessService(
|
||||
HarnessForegroundService.ACTION_START_FOREGROUND));
|
||||
addButton(root, "Post stopwatch/chronometer", v -> postChronometerNotification());
|
||||
addButton(root, "Stop media/foreground service", v -> stopHarnessService());
|
||||
|
||||
addSection(root, "Notifications");
|
||||
addButton(root, "Post normal notification", v -> postNotification(1, false, false, "Normal"));
|
||||
addButton(root, "Post ongoing notification", v -> postNotification(2, true, false, "Ongoing"));
|
||||
addButton(root, "Post grouped notifications", v -> postGroupedNotifications());
|
||||
addButton(root, "Update notification text", v -> {
|
||||
updatingCounter++;
|
||||
postNotification(3, false, true, "Updated " + updatingCounter);
|
||||
});
|
||||
addButton(root, "Start notification churn", v -> startNotificationChurn());
|
||||
addButton(root, "Stop notification churn", v -> stopNotificationChurn());
|
||||
addButton(root, "Clear harness notifications", v -> {
|
||||
stopNotificationChurn();
|
||||
clearNotifications();
|
||||
});
|
||||
|
||||
return scroll;
|
||||
}
|
||||
|
||||
private void handleCommand(Intent intent) {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
String command = intent.getStringExtra(EXTRA_COMMAND);
|
||||
if (command == null) {
|
||||
return;
|
||||
}
|
||||
switch (command) {
|
||||
case COMMAND_POST_NORMAL:
|
||||
postNotification(1, false, false, "Normal");
|
||||
break;
|
||||
case COMMAND_POST_ONGOING:
|
||||
postNotification(2, true, false, "Ongoing");
|
||||
break;
|
||||
case COMMAND_POST_GROUPED:
|
||||
postGroupedNotifications();
|
||||
break;
|
||||
case COMMAND_UPDATE:
|
||||
updatingCounter++;
|
||||
postNotification(3, false, true, "Updated " + updatingCounter);
|
||||
break;
|
||||
case COMMAND_START_CHRONOMETER:
|
||||
postChronometerNotification();
|
||||
break;
|
||||
case COMMAND_START_CHURN:
|
||||
startNotificationChurn();
|
||||
break;
|
||||
case COMMAND_STOP_CHURN:
|
||||
stopNotificationChurn();
|
||||
break;
|
||||
case COMMAND_START_MEDIA:
|
||||
startHarnessService(HarnessForegroundService.ACTION_START_MEDIA);
|
||||
break;
|
||||
case COMMAND_START_FOREGROUND:
|
||||
startHarnessService(HarnessForegroundService.ACTION_START_FOREGROUND);
|
||||
break;
|
||||
case COMMAND_STOP_SERVICE:
|
||||
stopHarnessService();
|
||||
break;
|
||||
case COMMAND_CLEAR:
|
||||
stopNotificationChurn();
|
||||
clearNotifications();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void addScenario(LinearLayout root, String label, String scenario) {
|
||||
addButton(root, label, v -> {
|
||||
Intent intent = new Intent(this, ScenarioActivity.class);
|
||||
intent.putExtra(ScenarioActivity.EXTRA_SCENARIO, scenario);
|
||||
startActivity(intent);
|
||||
});
|
||||
}
|
||||
|
||||
private void addSection(LinearLayout root, String label) {
|
||||
TextView view = new TextView(this);
|
||||
view.setText(label);
|
||||
view.setTextColor(0xff444444);
|
||||
view.setTextSize(17);
|
||||
view.setPadding(0, dp(18), 0, dp(6));
|
||||
root.addView(view, matchWrap());
|
||||
}
|
||||
|
||||
private void addButton(LinearLayout root, String label, View.OnClickListener listener) {
|
||||
Button button = new Button(this);
|
||||
button.setText(label);
|
||||
button.setAllCaps(false);
|
||||
button.setOnClickListener(listener);
|
||||
LinearLayout.LayoutParams params = matchWrap();
|
||||
params.setMargins(0, dp(4), 0, dp(4));
|
||||
root.addView(button, params);
|
||||
}
|
||||
|
||||
private void postNotification(int slot, boolean ongoing, boolean alertOnlyOnce, String label) {
|
||||
requestNotificationPermissionIfNeeded();
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm == null || !canPostNotifications()) {
|
||||
return;
|
||||
}
|
||||
ensureChannel(nm);
|
||||
Intent launch = new Intent(this, MainActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
slot,
|
||||
launch,
|
||||
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentTitle("Harness " + label)
|
||||
.setContentText("Package: " + getPackageName())
|
||||
.setContentIntent(pendingIntent)
|
||||
.setShowWhen(true)
|
||||
.setOnlyAlertOnce(alertOnlyOnce)
|
||||
.setCategory(Notification.CATEGORY_STATUS);
|
||||
if (ongoing) {
|
||||
builder.setOngoing(true);
|
||||
}
|
||||
nm.notify(NOTIFICATION_BASE_ID + slot, builder.build());
|
||||
}
|
||||
|
||||
private void postGroupedNotifications() {
|
||||
requestNotificationPermissionIfNeeded();
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm == null || !canPostNotifications()) {
|
||||
return;
|
||||
}
|
||||
ensureChannel(nm);
|
||||
String group = "sbt_harness_group";
|
||||
for (int i = 0; i < 3; i++) {
|
||||
Notification notification = new Notification.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentTitle("Harness grouped " + (i + 1))
|
||||
.setContentText("Grouped notification")
|
||||
.setGroup(group)
|
||||
.setShowWhen(false)
|
||||
.build();
|
||||
nm.notify(NOTIFICATION_BASE_ID + 10 + i, notification);
|
||||
}
|
||||
Notification summary = new Notification.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentTitle("Harness group")
|
||||
.setContentText("Group summary")
|
||||
.setGroup(group)
|
||||
.setGroupSummary(true)
|
||||
.build();
|
||||
nm.notify(NOTIFICATION_BASE_ID + 20, summary);
|
||||
}
|
||||
|
||||
private void postChronometerNotification() {
|
||||
requestNotificationPermissionIfNeeded();
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm == null || !canPostNotifications()) {
|
||||
return;
|
||||
}
|
||||
ensureChannel(nm);
|
||||
Notification notification = new Notification.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentTitle("Harness stopwatch")
|
||||
.setContentText("Chronometer notification active")
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setUsesChronometer(true)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setCategory(Notification.CATEGORY_STATUS)
|
||||
.build();
|
||||
nm.notify(NOTIFICATION_BASE_ID + 30, notification);
|
||||
}
|
||||
|
||||
private void startNotificationChurn() {
|
||||
requestNotificationPermissionIfNeeded();
|
||||
if (!canPostNotifications()) {
|
||||
return;
|
||||
}
|
||||
if (churnRunnable != null) {
|
||||
return;
|
||||
}
|
||||
churnRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
churnCounter++;
|
||||
postNotification(40, false, true, "Churn " + churnCounter);
|
||||
postNotification(41, true, true, "Ongoing churn " + churnCounter);
|
||||
if (churnCounter % 2 == 0) {
|
||||
postNotification(42, false, true, "Intermittent " + churnCounter);
|
||||
} else {
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm != null) {
|
||||
nm.cancel(NOTIFICATION_BASE_ID + 42);
|
||||
}
|
||||
}
|
||||
handler.postDelayed(this, 1000);
|
||||
}
|
||||
};
|
||||
churnRunnable.run();
|
||||
}
|
||||
|
||||
private void stopNotificationChurn() {
|
||||
if (churnRunnable != null) {
|
||||
handler.removeCallbacks(churnRunnable);
|
||||
churnRunnable = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void startHarnessService(String action) {
|
||||
requestNotificationPermissionIfNeeded();
|
||||
if (!canPostNotifications()) {
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, HarnessForegroundService.class);
|
||||
intent.setAction(action);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent);
|
||||
} else {
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopHarnessService() {
|
||||
Intent intent = new Intent(this, HarnessForegroundService.class);
|
||||
intent.setAction(HarnessForegroundService.ACTION_STOP);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void clearNotifications() {
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm != null) {
|
||||
nm.cancelAll();
|
||||
}
|
||||
stopHarnessService();
|
||||
}
|
||||
|
||||
private void ensureChannel(NotificationManager nm) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"StatusBarTweak harness",
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel.setDescription("Scenario notifications for StatusBarTweak compatibility testing");
|
||||
channel.setShowBadge(false);
|
||||
nm.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
private void requestNotificationPermissionIfNeeded() {
|
||||
if (Build.VERSION.SDK_INT < 33 || canPostNotifications()) {
|
||||
return;
|
||||
}
|
||||
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 200);
|
||||
}
|
||||
|
||||
private boolean canPostNotifications() {
|
||||
return Build.VERSION.SDK_INT < 33
|
||||
|| checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private LinearLayout.LayoutParams matchWrap() {
|
||||
return new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
|
||||
private int dp(int value) {
|
||||
return Math.round(value * getResources().getDisplayMetrics().density);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
package se.ajpanton.statusbartweak.harness;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.hardware.camera2.CameraAccessException;
|
||||
import android.hardware.camera2.CameraDevice;
|
||||
import android.hardware.camera2.CameraManager;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioRecord;
|
||||
import android.media.MediaRecorder;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowInsetsController;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
public final class ScenarioActivity extends Activity {
|
||||
public static final String EXTRA_SCENARIO = "scenario";
|
||||
public static final String SCENARIO_OPAQUE_LIGHT = "opaque_light";
|
||||
public static final String SCENARIO_OPAQUE_DARK = "opaque_dark";
|
||||
public static final String SCENARIO_TRANSPARENT = "transparent";
|
||||
public static final String SCENARIO_FULLSCREEN_PORTRAIT = "fullscreen_portrait";
|
||||
public static final String SCENARIO_FULLSCREEN_LANDSCAPE = "fullscreen_landscape";
|
||||
public static final String SCENARIO_CAMERA = "camera";
|
||||
public static final String SCENARIO_MICROPHONE = "microphone";
|
||||
public static final String SCENARIO_CAMERA_MICROPHONE = "camera_microphone";
|
||||
|
||||
private static final int REQUEST_CAMERA = 301;
|
||||
private static final int REQUEST_MICROPHONE = 302;
|
||||
private static final int REQUEST_CAMERA_MICROPHONE = 303;
|
||||
|
||||
private HandlerThread cameraThread;
|
||||
private Handler cameraHandler;
|
||||
private CameraDevice cameraDevice;
|
||||
private AudioRecord audioRecord;
|
||||
private Thread audioThread;
|
||||
private volatile boolean audioRunning;
|
||||
private TextView statusText;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
String scenario = getIntent().getStringExtra(EXTRA_SCENARIO);
|
||||
if (scenario == null) {
|
||||
scenario = SCENARIO_OPAQUE_LIGHT;
|
||||
}
|
||||
final String activeScenario = scenario;
|
||||
boolean lightBars = configureWindow(activeScenario);
|
||||
View content = createScenarioView(activeScenario);
|
||||
setContentView(content);
|
||||
content.post(() -> {
|
||||
setLightBars(lightBars);
|
||||
if (isFullscreen(activeScenario)) {
|
||||
hideSystemBars();
|
||||
}
|
||||
});
|
||||
startScenarioIfNeeded(activeScenario);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
stopCamera();
|
||||
stopMicrophone();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
boolean granted = true;
|
||||
for (int result : grantResults) {
|
||||
granted &= result == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
granted &= grantResults.length > 0;
|
||||
if (!granted) {
|
||||
setStatus("Permission denied.");
|
||||
return;
|
||||
}
|
||||
if (requestCode == REQUEST_CAMERA) {
|
||||
startCamera();
|
||||
} else if (requestCode == REQUEST_MICROPHONE) {
|
||||
startMicrophone();
|
||||
} else if (requestCode == REQUEST_CAMERA_MICROPHONE) {
|
||||
startCameraAndMicrophone();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean configureWindow(String scenario) {
|
||||
Window window = getWindow();
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
window.getAttributes().layoutInDisplayCutoutMode =
|
||||
android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
}
|
||||
|
||||
if (SCENARIO_FULLSCREEN_LANDSCAPE.equals(scenario)) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||
} else if (SCENARIO_FULLSCREEN_PORTRAIT.equals(scenario)) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
|
||||
if (SCENARIO_OPAQUE_LIGHT.equals(scenario)) {
|
||||
window.setStatusBarColor(Color.WHITE);
|
||||
window.setNavigationBarColor(Color.WHITE);
|
||||
return true;
|
||||
} else if (SCENARIO_OPAQUE_DARK.equals(scenario)) {
|
||||
window.setStatusBarColor(Color.BLACK);
|
||||
window.setNavigationBarColor(Color.BLACK);
|
||||
return false;
|
||||
} else if (SCENARIO_TRANSPARENT.equals(scenario)) {
|
||||
window.setStatusBarColor(Color.TRANSPARENT);
|
||||
window.setNavigationBarColor(Color.TRANSPARENT);
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
window.setDecorFitsSystemWindows(false);
|
||||
}
|
||||
return false;
|
||||
} else if (isFullscreen(scenario)) {
|
||||
window.setStatusBarColor(Color.TRANSPARENT);
|
||||
window.setNavigationBarColor(Color.BLACK);
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
window.setDecorFitsSystemWindows(false);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
window.setStatusBarColor(0xff202124);
|
||||
window.setNavigationBarColor(0xff202124);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private View createScenarioView(String scenario) {
|
||||
LinearLayout root = new LinearLayout(this);
|
||||
root.setOrientation(LinearLayout.VERTICAL);
|
||||
root.setGravity(Gravity.CENTER);
|
||||
root.setPadding(dp(24), dp(60), dp(24), dp(60));
|
||||
root.setBackgroundColor(backgroundColor(scenario));
|
||||
|
||||
TextView title = new TextView(this);
|
||||
title.setText(titleFor(scenario));
|
||||
title.setTextColor(textColor(scenario));
|
||||
title.setTextSize(28);
|
||||
title.setGravity(Gravity.CENTER);
|
||||
root.addView(title, matchWrap());
|
||||
|
||||
statusText = new TextView(this);
|
||||
statusText.setText(statusFor(scenario));
|
||||
statusText.setTextColor(textColor(scenario));
|
||||
statusText.setAlpha(0.85f);
|
||||
statusText.setTextSize(16);
|
||||
statusText.setGravity(Gravity.CENTER);
|
||||
statusText.setPadding(0, dp(12), 0, dp(20));
|
||||
root.addView(statusText, matchWrap());
|
||||
|
||||
if (isFullscreen(scenario)) {
|
||||
addButton(root, "Temporarily show system bars", v -> showSystemBarsBriefly());
|
||||
}
|
||||
if (SCENARIO_CAMERA.equals(scenario)) {
|
||||
addButton(root, "Start camera privacy indicator", v -> startCamera());
|
||||
addButton(root, "Stop camera", v -> stopCamera());
|
||||
}
|
||||
if (SCENARIO_MICROPHONE.equals(scenario)) {
|
||||
addButton(root, "Start microphone privacy indicator", v -> startMicrophone());
|
||||
addButton(root, "Stop microphone", v -> stopMicrophone());
|
||||
}
|
||||
if (SCENARIO_CAMERA_MICROPHONE.equals(scenario)) {
|
||||
addButton(root, "Start both indicators", v -> startCameraAndMicrophone());
|
||||
addButton(root, "Stop both", v -> {
|
||||
stopCamera();
|
||||
stopMicrophone();
|
||||
});
|
||||
}
|
||||
addButton(root, "Finish scenario", v -> finish());
|
||||
return root;
|
||||
}
|
||||
|
||||
private void startScenarioIfNeeded(String scenario) {
|
||||
if (SCENARIO_CAMERA.equals(scenario)) {
|
||||
startCamera();
|
||||
} else if (SCENARIO_MICROPHONE.equals(scenario)) {
|
||||
startMicrophone();
|
||||
} else if (SCENARIO_CAMERA_MICROPHONE.equals(scenario)) {
|
||||
startCameraAndMicrophone();
|
||||
}
|
||||
}
|
||||
|
||||
private int backgroundColor(String scenario) {
|
||||
if (SCENARIO_OPAQUE_LIGHT.equals(scenario)) {
|
||||
return Color.WHITE;
|
||||
}
|
||||
if (SCENARIO_TRANSPARENT.equals(scenario)) {
|
||||
return 0xff0d47a1;
|
||||
}
|
||||
if (isFullscreen(scenario)) {
|
||||
return 0xff1b5e20;
|
||||
}
|
||||
return 0xff101214;
|
||||
}
|
||||
|
||||
private int textColor(String scenario) {
|
||||
return SCENARIO_OPAQUE_LIGHT.equals(scenario) ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
|
||||
private String titleFor(String scenario) {
|
||||
switch (scenario) {
|
||||
case SCENARIO_OPAQUE_LIGHT:
|
||||
return "Opaque light statusbar";
|
||||
case SCENARIO_OPAQUE_DARK:
|
||||
return "Opaque dark statusbar";
|
||||
case SCENARIO_TRANSPARENT:
|
||||
return "Transparent statusbar";
|
||||
case SCENARIO_FULLSCREEN_PORTRAIT:
|
||||
return "Fullscreen portrait";
|
||||
case SCENARIO_FULLSCREEN_LANDSCAPE:
|
||||
return "Fullscreen landscape";
|
||||
case SCENARIO_CAMERA:
|
||||
return "Camera privacy indicator";
|
||||
case SCENARIO_MICROPHONE:
|
||||
return "Microphone privacy indicator";
|
||||
case SCENARIO_CAMERA_MICROPHONE:
|
||||
return "Camera + microphone indicators";
|
||||
default:
|
||||
return "Scenario";
|
||||
}
|
||||
}
|
||||
|
||||
private String statusFor(String scenario) {
|
||||
if (isFullscreen(scenario)) {
|
||||
return "System bars are hidden. Swipe from an edge or use the button below to reveal them.";
|
||||
}
|
||||
if (SCENARIO_TRANSPARENT.equals(scenario)) {
|
||||
return "Content is laid out behind a transparent statusbar.";
|
||||
}
|
||||
if (SCENARIO_CAMERA.equals(scenario)) {
|
||||
return "Camera should stay open until stopped or this screen is closed.";
|
||||
}
|
||||
if (SCENARIO_MICROPHONE.equals(scenario)) {
|
||||
return "AudioRecord should stay active until stopped or this screen is closed.";
|
||||
}
|
||||
if (SCENARIO_CAMERA_MICROPHONE.equals(scenario)) {
|
||||
return "Camera and AudioRecord should stay active until stopped or this screen is closed.";
|
||||
}
|
||||
return "This screen uses a normal app statusbar.";
|
||||
}
|
||||
|
||||
private void startCamera() {
|
||||
if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
|
||||
return;
|
||||
}
|
||||
if (cameraDevice != null) {
|
||||
setStatus("Camera already active.");
|
||||
return;
|
||||
}
|
||||
CameraManager manager = getSystemService(CameraManager.class);
|
||||
if (manager == null) {
|
||||
setStatus("CameraManager unavailable.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String[] ids = manager.getCameraIdList();
|
||||
if (ids.length == 0) {
|
||||
setStatus("No camera reported by this device.");
|
||||
return;
|
||||
}
|
||||
cameraThread = new HandlerThread("SbtHarnessCamera");
|
||||
cameraThread.start();
|
||||
cameraHandler = new Handler(cameraThread.getLooper());
|
||||
manager.openCamera(ids[0], new CameraDevice.StateCallback() {
|
||||
@Override
|
||||
public void onOpened(CameraDevice camera) {
|
||||
cameraDevice = camera;
|
||||
setStatus("Camera active: " + ids[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected(CameraDevice camera) {
|
||||
stopCamera();
|
||||
setStatus("Camera disconnected.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(CameraDevice camera, int error) {
|
||||
stopCamera();
|
||||
setStatus("Camera error: " + error);
|
||||
}
|
||||
}, cameraHandler);
|
||||
} catch (CameraAccessException | SecurityException | IllegalArgumentException e) {
|
||||
stopCamera();
|
||||
setStatus("Camera failed: " + e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
private void startCameraAndMicrophone() {
|
||||
boolean hasCamera = checkSelfPermission(Manifest.permission.CAMERA)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
boolean hasMic = checkSelfPermission(Manifest.permission.RECORD_AUDIO)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
if (!hasCamera || !hasMic) {
|
||||
requestPermissions(
|
||||
new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO},
|
||||
REQUEST_CAMERA_MICROPHONE);
|
||||
return;
|
||||
}
|
||||
startCamera();
|
||||
startMicrophone();
|
||||
}
|
||||
|
||||
private void stopCamera() {
|
||||
if (cameraDevice != null) {
|
||||
cameraDevice.close();
|
||||
cameraDevice = null;
|
||||
}
|
||||
if (cameraThread != null) {
|
||||
cameraThread.quitSafely();
|
||||
cameraThread = null;
|
||||
cameraHandler = null;
|
||||
}
|
||||
setStatus("Camera stopped.");
|
||||
}
|
||||
|
||||
private void startMicrophone() {
|
||||
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_MICROPHONE);
|
||||
return;
|
||||
}
|
||||
if (audioRecord != null) {
|
||||
setStatus("Microphone already active.");
|
||||
return;
|
||||
}
|
||||
int sampleRate = 8000;
|
||||
int minBuffer = AudioRecord.getMinBufferSize(
|
||||
sampleRate,
|
||||
AudioFormat.CHANNEL_IN_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT);
|
||||
int bufferSize = Math.max(minBuffer, 2048);
|
||||
try {
|
||||
audioRecord = new AudioRecord(
|
||||
MediaRecorder.AudioSource.MIC,
|
||||
sampleRate,
|
||||
AudioFormat.CHANNEL_IN_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
bufferSize);
|
||||
audioRecord.startRecording();
|
||||
audioRunning = true;
|
||||
audioThread = new Thread(() -> {
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
while (audioRunning && audioRecord != null) {
|
||||
audioRecord.read(buffer, 0, buffer.length);
|
||||
}
|
||||
}, "SbtHarnessMic");
|
||||
audioThread.start();
|
||||
setStatus("Microphone active.");
|
||||
} catch (SecurityException | IllegalStateException | IllegalArgumentException e) {
|
||||
stopMicrophone();
|
||||
setStatus("Microphone failed: " + e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
private void stopMicrophone() {
|
||||
audioRunning = false;
|
||||
if (audioRecord != null) {
|
||||
try {
|
||||
audioRecord.stop();
|
||||
} catch (IllegalStateException ignored) {
|
||||
}
|
||||
audioRecord.release();
|
||||
audioRecord = null;
|
||||
}
|
||||
audioThread = null;
|
||||
setStatus("Microphone stopped.");
|
||||
}
|
||||
|
||||
private void setStatus(String status) {
|
||||
if (statusText != null) {
|
||||
runOnUiThread(() -> statusText.setText(status));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFullscreen(String scenario) {
|
||||
return SCENARIO_FULLSCREEN_PORTRAIT.equals(scenario)
|
||||
|| SCENARIO_FULLSCREEN_LANDSCAPE.equals(scenario);
|
||||
}
|
||||
|
||||
private void hideSystemBars() {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
WindowInsetsController controller = getWindow().getInsetsController();
|
||||
if (controller != null) {
|
||||
controller.setSystemBarsBehavior(
|
||||
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
controller.hide(WindowInsets.Type.systemBars());
|
||||
}
|
||||
} else {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void showSystemBarsBriefly() {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
WindowInsetsController controller = getWindow().getInsetsController();
|
||||
if (controller != null) {
|
||||
controller.show(WindowInsets.Type.systemBars());
|
||||
}
|
||||
} else {
|
||||
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
}
|
||||
getWindow().getDecorView().postDelayed(this::hideSystemBars, 2500);
|
||||
}
|
||||
|
||||
private void setLightBars(boolean lightBars) {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
WindowInsetsController controller = getWindow().getInsetsController();
|
||||
if (controller != null) {
|
||||
int mask = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
||||
| WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
|
||||
controller.setSystemBarsAppearance(lightBars ? mask : 0, mask);
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= 23) {
|
||||
int flags = getWindow().getDecorView().getSystemUiVisibility();
|
||||
if (lightBars) {
|
||||
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
||||
}
|
||||
} else {
|
||||
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
||||
}
|
||||
}
|
||||
getWindow().getDecorView().setSystemUiVisibility(flags);
|
||||
}
|
||||
}
|
||||
|
||||
private void addButton(LinearLayout root, String label, View.OnClickListener listener) {
|
||||
Button button = new Button(this);
|
||||
button.setText(label);
|
||||
button.setAllCaps(false);
|
||||
button.setOnClickListener(listener);
|
||||
LinearLayout.LayoutParams params = matchWrap();
|
||||
params.setMargins(0, dp(8), 0, 0);
|
||||
root.addView(button, params);
|
||||
}
|
||||
|
||||
private LinearLayout.LayoutParams matchWrap() {
|
||||
return new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
|
||||
private int dp(int value) {
|
||||
return Math.round(value * getResources().getDisplayMetrics().density);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#202124"
|
||||
android:pathData="M4,4h40v40h-40z" />
|
||||
<path
|
||||
android:fillColor="#00E676"
|
||||
android:pathData="M4,4h32v5h-32z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M8,16h32v4h-32zM8,24h24v4h-24zM8,32h16v4h-16z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M4,5h16v11h-12l-4,4z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="harness_accent">#00E676</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">SBT Test Harness</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.StatusBarTweakHarness" parent="@android:style/Theme.Material.NoActionBar">
|
||||
<item name="android:fontFamily">sans</item>
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
<item name="android:navigationBarColor">#111111</item>
|
||||
<item name="android:colorAccent">@color/harness_accent</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user