mirror of
https://github.com/Benjamin-Wiegand/Flywheel.git
synced 2026-08-07 14:22:23 -07:00
feat: allow answering incoming calls
there is no in-call ui and likely won't be for a while. even this basic implementation will probably break if you receive two calls that ring at the same time.
This commit is contained in:
@@ -15,6 +15,9 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_CALL_LOG" tools:ignore="SmsAndCallLogPolicy" />
|
||||
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
|
||||
|
||||
<!-- wireless connection -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.benwiegand.projection.geargrinder.callback.MakeshiftBindCallback;
|
||||
import io.benwiegand.projection.geargrinder.pm.AppRecord;
|
||||
import io.benwiegand.projection.geargrinder.projection.ui.BatteryIndicator;
|
||||
import io.benwiegand.projection.geargrinder.projection.ui.NotificationDisplay;
|
||||
import io.benwiegand.projection.geargrinder.projection.ui.PhoneCallDisplay;
|
||||
import io.benwiegand.projection.geargrinder.projection.ui.task.ProjectionTask;
|
||||
import io.benwiegand.projection.geargrinder.projection.ui.NetworkIndicators;
|
||||
import io.benwiegand.projection.geargrinder.projection.ui.task.ProjectionTaskManager;
|
||||
@@ -55,6 +56,7 @@ public class ProjectionActivity extends AppCompatActivity implements MakeshiftBi
|
||||
private BatteryIndicator batteryIndicator;
|
||||
private NetworkIndicators networkIndicators;
|
||||
private NotificationDisplay notificationDisplay;
|
||||
private PhoneCallDisplay phoneCallDisplay;
|
||||
|
||||
private GeargrinderServiceConnector connector;
|
||||
private IPrivd privd = null;
|
||||
@@ -114,6 +116,7 @@ public class ProjectionActivity extends AppCompatActivity implements MakeshiftBi
|
||||
batteryIndicator = new BatteryIndicator(findViewById(R.id.battery_indicator));
|
||||
networkIndicators = new NetworkIndicators(findViewById(R.id.network_indicators));
|
||||
notificationDisplay = new NotificationDisplay(findViewById(R.id.popup_notification_overlay));
|
||||
phoneCallDisplay = new PhoneCallDisplay(this, findViewById(R.id.popup_incoming_call_overlay));
|
||||
|
||||
taskManager.registerListener(this);
|
||||
|
||||
@@ -155,6 +158,7 @@ public class ProjectionActivity extends AppCompatActivity implements MakeshiftBi
|
||||
batteryIndicator.destroy();
|
||||
networkIndicators.destroy();
|
||||
notificationDisplay.destroy();
|
||||
phoneCallDisplay.destroy();
|
||||
}
|
||||
|
||||
private boolean focusSearch(int direction) {
|
||||
|
||||
+21
-3
@@ -45,10 +45,25 @@ public class PermissionRequirements {
|
||||
settingsManager -> true);
|
||||
|
||||
public static final PermissionEntry READ_PHONE_STATE_PERMISSION_ENTRY = PermissionEntry.createForManifestPermission(
|
||||
Manifest.permission.READ_PHONE_STATE, Build.VERSION_CODES.Q, Build.VERSION_CODES.R,
|
||||
Manifest.permission.READ_PHONE_STATE, null, null,
|
||||
R.string.read_phone_state_permission_title, R.string.read_phone_state_permission_rationale,
|
||||
settingsManager -> true, settingsManager -> false);
|
||||
|
||||
public static final PermissionEntry ANSWER_PHONE_CALLS_PERMISSION_ENTRY = PermissionEntry.createForManifestPermission(
|
||||
Manifest.permission.ANSWER_PHONE_CALLS, null, null,
|
||||
R.string.answer_phone_calls_permission_title, R.string.answer_phone_calls_permission_rationale,
|
||||
settingsManager -> true, settingsManager -> false);
|
||||
|
||||
public static final PermissionEntry READ_CALL_LOG_PERMISSION_ENTRY = PermissionEntry.createForManifestPermission(
|
||||
Manifest.permission.READ_CALL_LOG, null, null,
|
||||
R.string.read_call_log_permission_title, R.string.read_call_log_permission_rationale,
|
||||
settingsManager -> true, settingsManager -> false);
|
||||
|
||||
public static final PermissionEntry READ_CONTACTS_PERMISSION_ENTRY = PermissionEntry.createForManifestPermission(
|
||||
Manifest.permission.READ_CONTACTS, null, null,
|
||||
R.string.read_contacts_permission_title, R.string.read_contacts_permission_rationale,
|
||||
settingsManager -> true, settingsManager -> false);
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
public static final PermissionEntry BLUETOOTH_CONNECT_PERMISSION_ENTRY = PermissionEntry.createForManifestPermission(
|
||||
Manifest.permission.BLUETOOTH_CONNECT, Build.VERSION_CODES.S, null,
|
||||
@@ -161,8 +176,11 @@ public class PermissionRequirements {
|
||||
POST_NOTIFICATIONS_PERMISSION_ENTRY,
|
||||
RECORD_AUDIO_PERMISSION_ENTRY,
|
||||
NOTIFICATION_LISTENER_PERMISSION_ENTRY,
|
||||
READ_PHONE_STATE_PERMISSION_ENTRY,
|
||||
ANSWER_PHONE_CALLS_PERMISSION_ENTRY,
|
||||
READ_CALL_LOG_PERMISSION_ENTRY,
|
||||
READ_CONTACTS_PERMISSION_ENTRY,
|
||||
BLUETOOTH_CONNECT_PERMISSION_ENTRY,
|
||||
ACCESS_FINE_LOCATION_PERMISSION_ENTRY,
|
||||
READ_PHONE_STATE_PERMISSION_ENTRY
|
||||
ACCESS_FINE_LOCATION_PERMISSION_ENTRY
|
||||
);
|
||||
}
|
||||
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
package io.benwiegand.projection.geargrinder.projection.ui;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import android.telecom.TelecomManager;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.R;
|
||||
|
||||
public class PhoneCallDisplay {
|
||||
private static final String TAG = PhoneCallDisplay.class.getSimpleName();
|
||||
|
||||
private static final long INCOMING_CALL_OVERLAY_ANIMATION_DURATION = 200;
|
||||
|
||||
private record CallerId(String phoneNumber, String displayName, Uri photoUri, Uri photoThumbnailUri) {
|
||||
public CallerId(String phoneNumber) {
|
||||
this(phoneNumber, null, null, null);
|
||||
}
|
||||
|
||||
public CallerId() {
|
||||
this(null, null, null, null);
|
||||
}
|
||||
|
||||
public String friendlyName(Context context) {
|
||||
if (displayName() != null) return displayName();
|
||||
if (phoneNumber() != null) return phoneNumber();
|
||||
return context.getString(R.string.unknown_caller);
|
||||
}
|
||||
|
||||
public Drawable thumbnailDrawable(Context context) {
|
||||
try {
|
||||
Uri uri = photoThumbnailUri() != null ? photoThumbnailUri() : photoUri();
|
||||
if (uri != null) {
|
||||
InputStream is = context.getContentResolver().openInputStream(uri);
|
||||
return Drawable.createFromStream(is, null);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "failed to load contact photo", t);
|
||||
}
|
||||
|
||||
return AppCompatResources.getDrawable(context, R.drawable.ic_launcher_foreground); // TODO
|
||||
}
|
||||
}
|
||||
|
||||
private static int callStateStringToInt(String callStateStr) {
|
||||
if (TelephonyManager.EXTRA_STATE_IDLE.equals(callStateStr)) return TelephonyManager.CALL_STATE_IDLE;
|
||||
if (TelephonyManager.EXTRA_STATE_RINGING.equals(callStateStr)) return TelephonyManager.CALL_STATE_RINGING;
|
||||
if (TelephonyManager.EXTRA_STATE_OFFHOOK.equals(callStateStr)) return TelephonyManager.CALL_STATE_OFFHOOK;
|
||||
Log.wtf(TAG, "unknown call state: " + callStateStr);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
private final Context context;
|
||||
private final ViewGroup popupIncomingCallOverlay;
|
||||
private final TelephonyManager telephonyManager;
|
||||
private final TelecomManager telecomManager;
|
||||
|
||||
private int callState = TelephonyManager.CALL_STATE_IDLE;
|
||||
private CallerId callerId = new CallerId();
|
||||
|
||||
private final BroadcastReceiver phoneStateReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d(TAG, "phone state update: " + intent);
|
||||
Log.d(TAG, "extras: " + intent.getExtras());
|
||||
if (intent.getExtras() != null) {
|
||||
for (String key : intent.getExtras().keySet()) {
|
||||
Log.v(TAG, "- " + key + " = " + intent.getStringExtra(key));
|
||||
}
|
||||
}
|
||||
|
||||
if (!TelephonyManager.ACTION_PHONE_STATE_CHANGED.equals(intent.getAction())) return;
|
||||
|
||||
String phoneNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
|
||||
int newCallState = callStateStringToInt(intent.getStringExtra(TelephonyManager.EXTRA_STATE));
|
||||
if (newCallState == -1) {
|
||||
newCallState = telephonyManager.getCallState();
|
||||
Log.w(TAG, "using call state from telephony manager: " + newCallState);
|
||||
}
|
||||
|
||||
boolean callerIdUpdated = false;
|
||||
|
||||
if (callState == TelephonyManager.CALL_STATE_IDLE) {
|
||||
callerIdUpdated = true;
|
||||
callerId = new CallerId();
|
||||
}
|
||||
|
||||
if (phoneNumber != null && !phoneNumber.equals(callerId.phoneNumber())) {
|
||||
callerIdUpdated = true;
|
||||
callerId = queryCallerId(phoneNumber);
|
||||
}
|
||||
|
||||
if (callerIdUpdated) onCallerIdUpdated();
|
||||
|
||||
callState = newCallState;
|
||||
switch (callState) {
|
||||
case TelephonyManager.CALL_STATE_IDLE -> onCallIdle();
|
||||
case TelephonyManager.CALL_STATE_RINGING -> onCallRinging();
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK -> onCallOffHook();
|
||||
default -> Log.wtf(TAG, "unhandled call state: " + callState, new AssertionError());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public PhoneCallDisplay(Context context, ViewGroup popupIncomingCallOverlay) {
|
||||
this.context = context;
|
||||
this.popupIncomingCallOverlay = popupIncomingCallOverlay;
|
||||
|
||||
telephonyManager = context.getSystemService(TelephonyManager.class);
|
||||
telecomManager = context.getSystemService(TelecomManager.class);
|
||||
|
||||
inflateIncomingCallPopup();
|
||||
|
||||
context.registerReceiver(phoneStateReceiver, new IntentFilter(TelephonyManager.ACTION_PHONE_STATE_CHANGED));
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
context.unregisterReceiver(phoneStateReceiver);
|
||||
}
|
||||
|
||||
private void animateIncomingCallOverlay(boolean show) {
|
||||
popupIncomingCallOverlay.animate()
|
||||
.setStartDelay(0)
|
||||
.setDuration(INCOMING_CALL_OVERLAY_ANIMATION_DURATION)
|
||||
.withStartAction(() -> { if (show) popupIncomingCallOverlay.setVisibility(View.VISIBLE); })
|
||||
.withEndAction(() -> { if (!show) popupIncomingCallOverlay.setVisibility(View.GONE); })
|
||||
.alpha(show ? 1 : 0)
|
||||
.translationX(show ? 0 : popupIncomingCallOverlay.getWidth());
|
||||
}
|
||||
|
||||
private void onCallerIdUpdated() {
|
||||
TextView cidText = popupIncomingCallOverlay.findViewById(R.id.caller_id_text);
|
||||
ImageView cidImage = popupIncomingCallOverlay.findViewById(R.id.caller_id_image);
|
||||
cidText.setText(callerId.friendlyName(context));
|
||||
cidImage.setImageDrawable(callerId.thumbnailDrawable(context));
|
||||
}
|
||||
|
||||
private void onCallRinging() {
|
||||
animateIncomingCallOverlay(true);
|
||||
}
|
||||
|
||||
private void onCallOffHook() {
|
||||
animateIncomingCallOverlay(false);
|
||||
}
|
||||
|
||||
private void onCallIdle() {
|
||||
animateIncomingCallOverlay(false);
|
||||
}
|
||||
|
||||
private CallerId queryCallerId(String phoneNumber) {
|
||||
Log.d(TAG, "finding contact for " + phoneNumber);
|
||||
String[] projection = new String[] {
|
||||
ContactsContract.PhoneLookup._ID,
|
||||
ContactsContract.PhoneLookup.DISPLAY_NAME_PRIMARY,
|
||||
ContactsContract.PhoneLookup.PHOTO_URI,
|
||||
ContactsContract.PhoneLookup.PHOTO_THUMBNAIL_URI,
|
||||
};
|
||||
try (Cursor cur = context.getContentResolver().query(Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)), projection, null, null, null)) {
|
||||
if (cur == null || !cur.moveToFirst()) {
|
||||
Log.e(TAG, "contact lookup failed: " + (cur == null ? "null result" : "no results"));
|
||||
return new CallerId(phoneNumber);
|
||||
}
|
||||
|
||||
String displayName = cur.getString(1);
|
||||
String photoUri = cur.getString(2);
|
||||
String photoThumbUri = cur.getString(3);
|
||||
|
||||
return new CallerId(
|
||||
phoneNumber, displayName,
|
||||
photoUri != null ? Uri.parse(photoUri) : null,
|
||||
photoThumbUri != null ? Uri.parse(photoThumbUri) : null
|
||||
);
|
||||
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "contact lookup threw", t);
|
||||
}
|
||||
|
||||
return new CallerId(phoneNumber);
|
||||
}
|
||||
|
||||
private void inflateIncomingCallPopup() {
|
||||
View acceptCallButton = popupIncomingCallOverlay.findViewById(R.id.accept_call_button);
|
||||
View rejectCallButton = popupIncomingCallOverlay.findViewById(R.id.reject_call_button);
|
||||
View fallbackNotice = popupIncomingCallOverlay.findViewById(R.id.incoming_call_fallback_notice);
|
||||
|
||||
Runnable showFallbackNotice = () -> {
|
||||
acceptCallButton.setVisibility(View.GONE);
|
||||
fallbackNotice.setVisibility(View.VISIBLE);
|
||||
};
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ANSWER_PHONE_CALLS) != PackageManager.PERMISSION_GRANTED)
|
||||
showFallbackNotice.run();
|
||||
|
||||
acceptCallButton.setOnClickListener(v -> {
|
||||
try {
|
||||
telecomManager.acceptRingingCall();
|
||||
} catch (Throwable t) {
|
||||
Log.wtf(TAG, "failed to accept call", t);
|
||||
showFallbackNotice.run();
|
||||
}
|
||||
});
|
||||
|
||||
rejectCallButton.setOnClickListener(v -> {
|
||||
// this doesn't work since if there's an active call and another incoming call it'll hang up the active call
|
||||
// try {
|
||||
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return;
|
||||
// telecomManager.endCall();
|
||||
// } catch (Throwable t) {
|
||||
// Log.wtf(TAG, "failed to reject call", t);
|
||||
// showFallbackNotice.run();
|
||||
// }
|
||||
|
||||
// unfortunately not much I can do here, USB doesn't count as a "companion device".
|
||||
// and figuring out how to do it via privd is a rabbit hole that I don't have the time for currently.
|
||||
// perhaps it could be doable with InCallService for wireless connections, but I don't actually use a wireless connection.
|
||||
animateIncomingCallOverlay(false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<corners android:radius="10dp" />
|
||||
<solid android:color="?attr/colorSurfaceContainer" />
|
||||
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<gradient android:startColor="#aa000000" android:endColor="#20000000" android:angle="180" />
|
||||
|
||||
</shape>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@@ -139,7 +140,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/popup_notification_overlay_background"
|
||||
android:background="@drawable/popup_top_overlay_background"
|
||||
android:alpha="0"
|
||||
android:visibility="gone"
|
||||
>
|
||||
@@ -160,4 +161,31 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/popup_incoming_call_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="48dp"
|
||||
android:background="@drawable/popup_right_overlay_background"
|
||||
android:alpha="0"
|
||||
android:visibility="gone"
|
||||
android:clickable="true"
|
||||
>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="right|bottom"
|
||||
android:padding="12dp"
|
||||
tools:ignore="RtlHardcoded"
|
||||
>
|
||||
|
||||
<include
|
||||
layout="@layout/layout_incoming_call_popup"
|
||||
/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp"
|
||||
android:background="@drawable/popup_floating_background"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:elevation="8dp"
|
||||
android:clickable="true"
|
||||
>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/caller_id_image"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_weight="9"
|
||||
android:importantForAccessibility="no"
|
||||
tools:src="@android:drawable/ic_menu_gallery"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/caller_id_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:padding="12dp"
|
||||
android:textAlignment="center"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
style="?textAppearanceHeadline5"
|
||||
tools:text="some caller"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/accept_call_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="0"
|
||||
android:background="@drawable/circle"
|
||||
android:backgroundTint="?colorAcceptCallButton"
|
||||
android:src="@android:drawable/stat_sys_phone_call"
|
||||
style="?imageButtonStyle"
|
||||
/>
|
||||
|
||||
<Space
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/reject_call_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="0"
|
||||
android:background="@drawable/circle"
|
||||
android:backgroundTint="?colorRejectCallButton"
|
||||
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
||||
style="?imageButtonStyle"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/incoming_call_fallback_notice"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/incoming_call_fallback_notice"
|
||||
android:visibility="gone"
|
||||
style="?textAppearanceBody1"
|
||||
tools:visibility="visible"
|
||||
/>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
@@ -17,6 +17,9 @@
|
||||
|
||||
<attr name="colorFocusIndicatorInactive" format="color" />
|
||||
<attr name="colorFocusIndicatorActive" format="color" />
|
||||
|
||||
<attr name="colorAcceptCallButton" format="color" />
|
||||
<attr name="colorRejectCallButton" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
@@ -58,9 +58,15 @@
|
||||
<string name="post_notifications_permission_title">Post notifications</string>
|
||||
<string name="post_notifications_permission_rationale">Allows Flywheel to run in the background, communicate errors, and show connection status. Things may not work properly without this permission.</string>
|
||||
<string name="read_phone_state_permission_title">Read phone state</string>
|
||||
<string name="read_phone_state_permission_rationale">Allows Flywheel to display more accurate telephone signal status on Android 11 and below.\n\nThis permission is optional.</string>
|
||||
<string name="read_phone_state_permission_rationale">Allows Flywheel to display incoming calls on your car.\n\nOn Android 11 and below, it is also used to show more accurate cell signal status.</string>
|
||||
<string name="record_audio_permission_title">Record audio</string>
|
||||
<string name="record_audio_permission_rationale">Allows Flywheel to capture device audio via screen sharing for playback through your car.</string>
|
||||
<string name="answer_phone_calls_permission_title">Answer phone calls</string>
|
||||
<string name="answer_phone_calls_permission_rationale">Allows Flywheel to show a button to accept incoming calls.</string>
|
||||
<string name="read_call_log_permission_title">Read call log</string>
|
||||
<string name="read_call_log_permission_rationale">Allows Flywheel to show the phone number or name for incoming calls.</string>
|
||||
<string name="read_contacts_permission_title">Read contacts</string>
|
||||
<string name="read_contacts_permission_rationale">Allows Flywheel to show the contact photo and display name for incoming calls.</string>
|
||||
<string name="bluetooth_connect_permission_title">Bluetooth connection</string>
|
||||
<string name="bluetooth_connect_permission_rationale">Allows Flywheel to connect to your car over Bluetooth, which is needed to start a wireless connection.\n\nThis is permission only needed if you want to connect wirelessly.</string>
|
||||
<string name="access_fine_location_permission_title">Fine location</string>
|
||||
@@ -177,4 +183,7 @@
|
||||
<string name="phone_audio_capture_name">Capture call audio (requires root or Shizuku)</string>
|
||||
<string name="phone_audio_capture_summary">play phone call audio through your car</string>
|
||||
|
||||
<string name="unknown_caller">Unknown</string>
|
||||
<string name="incoming_call_fallback_notice">when it is safe, answer on your phone</string>
|
||||
|
||||
</resources>
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
<item name="colorAppModal">?attr/colorSurfaceContainer</item>
|
||||
|
||||
<item name="colorAcceptCallButton">#23A023</item>
|
||||
<item name="colorRejectCallButton">#9E3434</item>
|
||||
|
||||
<item name="dynamicColorThemeOverlay">@style/ThemeOverlay.Geargrinder.DynamicColors</item>
|
||||
<item name="actionBarStyle">@style/ThemeOverlay.Geargrinder.ActionBar</item>
|
||||
<item name="alertDialogTheme">@style/ThemeOverlay.Geargrinder.AlertDialog</item>
|
||||
|
||||
Reference in New Issue
Block a user