2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 09:58:08 +00:00

Request notifications permission so we can target Android 13

This commit is contained in:
Albert Vaca Cintora 2023-08-21 22:53:26 +02:00
parent a74a76b1da
commit 8a4da371c8
19 changed files with 224 additions and 21 deletions

View File

@ -49,6 +49,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:icon="@mipmap/ic_launcher"

View File

@ -41,7 +41,7 @@ android {
compileSdk = 33
defaultConfig {
minSdk = 21
targetSdk = 32
targetSdk = 33
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
buildFeatures {

View File

@ -1,4 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:drawablePadding="8dp"
android:paddingTop="16dp"
android:paddingBottom="12dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:text="@string/no_notifications"
app:drawableStartCompat="@drawable/ic_warning"
app:drawableTint="?attr/colorControlNormal">
</TextView>

View File

@ -1,4 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2019 Matthijs Tijink <matthijstijink@gmail.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:drawablePadding="8dp"
android:paddingTop="16dp"
android:paddingBottom="12dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:text="@string/no_notifications"
app:drawableStartCompat="@drawable/ic_warning"
app:drawableLeftCompat="@drawable/ic_warning">
</TextView>

View File

@ -252,6 +252,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<item>60000000</item>
<item>120000000</item>
</string-array>
<string name="mpris_notifications_explanation">The notifications permission is needed to show remote media in the notifications drawer</string>
<string name="mpris_notification_settings_title">Show media control notification</string>
<string name="mpris_notification_settings_summary">Allow controlling your media players without opening KDE Connect</string>
<string name="mpris_notification_key" translatable="false">mpris_notification_enabled</string>
@ -331,6 +332,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<string name="optional_permission_explanation">You need to grant extra permissions to enable all functions</string>
<string name="plugins_need_optional_permission">Some plugins have features disabled because of lack of permission (tap for more info):</string>
<string name="share_optional_permission_explanation">To receive files you need to allow storage access</string>
<string name="share_notifications_explanation">To see the progress when sending and receiving files you need to allow notifications</string>
<string name="telepathy_permission_explanation">To read and write SMS from your desktop you need to give permission to SMS</string>
<string name="telephony_permission_explanation">To see phone calls on the desktop you need to give permission to phone call logs and phone state</string>
<string name="telephony_optional_permission_explanation">To see a contact name instead of a phone number you need to give access to the phone\'s contacts</string>
@ -547,4 +549,8 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<string name="plugin_stats">Plugin stats</string>
<string name="receive_notifications_permission_explanation">Notifications need to be allowed to receive them from other devices</string>
<string name="findmyphone_notifications_explanation">The notifications permission is needed so the phone can ring when the app is in the background</string>
<string name="no_notifications">Notifications are disabled, you won\'t receive incoming pair notifications.</string>
</resources>

View File

@ -67,8 +67,6 @@ public class BigscreenActivity extends AppCompatActivity {
new PermissionsAlertDialogFragment.Builder()
.setTitle(plugin.getDisplayName())
.setMessage(R.string.bigscreen_optional_permission_explanation)
.setPositiveButton(R.string.ok)
.setNegativeButton(R.string.cancel)
.setPermissions(new String[]{Manifest.permission.RECORD_AUDIO})
.setRequestCode(MainActivity.RESULT_NEEDS_RELOAD)
.create().show(getSupportFragmentManager(), null);

View File

@ -6,6 +6,7 @@
package org.kde.kdeconnect.Plugins.FindMyPhonePlugin;
import android.Manifest;
import android.app.Activity;
import android.app.NotificationManager;
import android.app.PendingIntent;
@ -120,6 +121,9 @@ public class FindMyPhonePlugin extends Plugin {
intent.putExtra(FindMyPhoneActivity.EXTRA_DEVICE_ID, device.getDeviceId());
context.startActivity(intent);
} else {
if (!checkOptionalPermissions()) {
return false;
}
if (powerManager.isInteractive()) {
startPlaying();
showBroadcastNotification();
@ -127,7 +131,6 @@ public class FindMyPhonePlugin extends Plugin {
showActivityNotification();
}
}
return true;
}
@ -219,4 +222,19 @@ public class FindMyPhonePlugin extends Plugin {
public PluginSettingsFragment getSettingsFragment(Activity activity) {
return FindMyPhoneSettingsFragment.newInstance(getPluginKey(), R.xml.findmyphoneplugin_preferences);
}
@NonNull
@Override
protected String[] getRequiredPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return new String[]{Manifest.permission.POST_NOTIFICATIONS};
} else {
return ArrayUtils.EMPTY_STRING_ARRAY;
}
}
@Override
protected int getPermissionExplanation() {
return R.string.findmyphone_notifications_explanation;
}
}

View File

@ -6,11 +6,13 @@
package org.kde.kdeconnect.Plugins.MprisPlugin;
import android.Manifest;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.os.Build;
@ -257,6 +259,14 @@ public class MprisMediaSession implements
*/
private void updateMediaNotification() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
int permissionResult = ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS);
if (permissionResult != PackageManager.PERMISSION_GRANTED) {
closeMediaNotification();
return;
}
}
//If the user disabled the media notification, do not show it
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (!prefs.getBoolean(context.getString(R.string.mpris_notification_key), true)) {

View File

@ -6,15 +6,18 @@
package org.kde.kdeconnect.Plugins.MprisPlugin;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Build;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import org.apache.commons.lang3.ArrayUtils;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
@ -521,4 +524,19 @@ public class MprisPlugin extends Plugin {
}
return false;
}
@NonNull
@Override
protected String[] getOptionalPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return new String[]{Manifest.permission.POST_NOTIFICATIONS};
} else {
return ArrayUtils.EMPTY_STRING_ARRAY;
}
}
@Override
protected int getOptionalPermissionExplanation() {
return R.string.mpris_notifications_explanation;
}
}

View File

@ -6,12 +6,18 @@
package org.kde.kdeconnect.Plugins.PingPlugin;
import android.Manifest;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
@ -66,6 +72,15 @@ public class PingPlugin extends Plugin {
id = 42; //A unique id to create only one notification
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
int permissionResult = ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS);
if (permissionResult != PackageManager.PERMISSION_GRANTED) {
// If notifications are not allowed, show a toast instead of a notification
new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show());
return true;
}
}
NotificationManager notificationManager = ContextCompat.getSystemService(context, NotificationManager.class);
Notification noti = new NotificationCompat.Builder(context, NotificationHelper.Channels.DEFAULT)

View File

@ -278,8 +278,6 @@ public abstract class Plugin {
return new PermissionsAlertDialogFragment.Builder()
.setTitle(getDisplayName())
.setMessage(reason)
.setPositiveButton(R.string.ok)
.setNegativeButton(R.string.cancel)
.setPermissions(permissions)
.setRequestCode(MainActivity.RESULT_NEEDS_RELOAD)
.create();

View File

@ -6,18 +6,21 @@
package org.kde.kdeconnect.Plugins.ReceiveNotificationsPlugin;
import android.Manifest;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import org.apache.commons.lang3.ArrayUtils;
import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
@ -122,4 +125,19 @@ public class ReceiveNotificationsPlugin extends Plugin {
public @NonNull String[] getOutgoingPacketTypes() {
return new String[]{PACKET_TYPE_NOTIFICATION_REQUEST};
}
@NonNull
@Override
protected String[] getRequiredPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return new String[]{Manifest.permission.POST_NOTIFICATIONS};
} else {
return ArrayUtils.EMPTY_STRING_ARRAY;
}
}
@Override
protected int getPermissionExplanation() {
return R.string.receive_notifications_permission_explanation;
}
}

View File

@ -75,8 +75,12 @@ public class SharePlugin extends Plugin {
@Override
protected int getOptionalPermissionExplanation() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return R.string.share_notifications_explanation;
} else {
return R.string.share_optional_permission_explanation;
}
}
@Override
public @NonNull String getDisplayName() {
@ -290,7 +294,9 @@ public class SharePlugin extends Plugin {
@Override
public @NonNull String[] getOptionalPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return new String[]{Manifest.permission.POST_NOTIFICATIONS};
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return ArrayUtils.EMPTY_STRING_ARRAY;
} else {
return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};

View File

@ -22,6 +22,8 @@ import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
@ -192,6 +194,15 @@ class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
else -> setContentFragment(PairingFragment())
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permissionResult = ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
if (permissionResult != PackageManager.PERMISSION_GRANTED) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), RESULT_NOTIFICATIONS_ENABLED)
}
}
}
}
override fun onDestroy() {
@ -324,25 +335,33 @@ class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
val uri = data.data
ShareSettingsFragment.saveStorageLocationPreference(this, uri)
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
fun isPermissionGranted(permissions: Array<String>, grantResults: IntArray, permission : String) : Boolean {
val index = ArrayUtils.indexOf(permissions, permission)
return index != ArrayUtils.INDEX_NOT_FOUND && grantResults[index] == PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
val permissionsGranted = ArrayUtils.contains(grantResults, PackageManager.PERMISSION_GRANTED)
if (permissionsGranted) {
val i = ArrayUtils.indexOf(permissions, Manifest.permission.WRITE_EXTERNAL_STORAGE)
val writeStoragePermissionGranted = i != ArrayUtils.INDEX_NOT_FOUND &&
grantResults[i] == PackageManager.PERMISSION_GRANTED
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && writeStoragePermissionGranted) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isPermissionGranted(permissions, grantResults, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// To get a writeable path manually on Android 10 and later for Share and Receive Plugin.
// Otherwise, Receiving files will keep failing until the user chooses a path manually to receive files.
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, STORAGE_LOCATION_CONFIGURED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isPermissionGranted(permissions, grantResults, Manifest.permission.POST_NOTIFICATIONS)) {
// If PairingFragment is active, reload it
if (mCurrentDevice == null) {
setContentFragment(PairingFragment())
}
}
//New permission granted, reload plugins
KdeConnect.getInstance().getDevice(mCurrentDevice)?.reloadPluginsFromSettings()
}
@ -370,6 +389,7 @@ class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
const val PAIRING_REJECTED = "rejected"
const val PAIRING_PENDING = "pending"
const val RESULT_NEEDS_RELOAD = RESULT_FIRST_USER
const val RESULT_NOTIFICATIONS_ENABLED = RESULT_FIRST_USER+1
const val FLAG_FORCE_OVERVIEW = "forceOverview"
}

View File

@ -6,10 +6,13 @@
package org.kde.kdeconnect.UserInterface;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.view.LayoutInflater;
@ -21,6 +24,8 @@ import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import org.kde.kdeconnect.BackgroundService;
@ -34,6 +39,7 @@ import org.kde.kdeconnect_tp.R;
import org.kde.kdeconnect_tp.databinding.DevicesListBinding;
import org.kde.kdeconnect_tp.databinding.PairingExplanationNotTrustedBinding;
import org.kde.kdeconnect_tp.databinding.PairingExplanationTextBinding;
import org.kde.kdeconnect_tp.databinding.PairingExplanationTextNoNotificationsBinding;
import org.kde.kdeconnect_tp.databinding.PairingExplanationTextNoWifiBinding;
import java.util.ArrayList;
@ -52,6 +58,7 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb
private PairingExplanationNotTrustedBinding pairingExplanationNotTrustedBinding;
private PairingExplanationTextBinding pairingExplanationTextBinding;
private PairingExplanationTextNoWifiBinding pairingExplanationTextNoWifiBinding;
private PairingExplanationTextNoNotificationsBinding pairingExplanationTextNoNotificationsBinding;
private MainActivity mActivity;
@ -59,6 +66,7 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb
private TextView headerText;
private TextView noWifiHeader;
private TextView noNotificationsHeader;
private TextView notTrustedText;
@Override
@ -69,23 +77,35 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb
setHasOptionsMenu(true);
devicesListBinding = DevicesListBinding.inflate(inflater, container, false);
pairingExplanationNotTrustedBinding = PairingExplanationNotTrustedBinding.inflate(inflater);
pairingExplanationTextBinding = PairingExplanationTextBinding.inflate(inflater);
pairingExplanationTextNoWifiBinding = PairingExplanationTextNoWifiBinding.inflate(inflater);
devicesListBinding.refreshListLayout.setOnRefreshListener(this::refreshDevicesAction);
notTrustedText = pairingExplanationNotTrustedBinding.getRoot();
notTrustedText.setOnClickListener(null);
notTrustedText.setOnLongClickListener(null);
pairingExplanationTextBinding = PairingExplanationTextBinding.inflate(inflater);
headerText = pairingExplanationTextBinding.getRoot();
headerText.setOnClickListener(null);
headerText.setOnLongClickListener(null);
pairingExplanationTextNoWifiBinding = PairingExplanationTextNoWifiBinding.inflate(inflater);
noWifiHeader = pairingExplanationTextNoWifiBinding.getRoot();
noWifiHeader.setOnClickListener(view -> startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS)));
pairingExplanationTextNoNotificationsBinding = PairingExplanationTextNoNotificationsBinding.inflate(inflater);
noNotificationsHeader = pairingExplanationTextNoNotificationsBinding.getRoot();
noNotificationsHeader.setOnClickListener(view -> ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.POST_NOTIFICATIONS}, MainActivity.RESULT_NOTIFICATIONS_ENABLED));
noNotificationsHeader.setOnLongClickListener(view -> {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", requireContext().getPackageName(), null);
intent.setData(uri);
startActivity(intent);
return true;
});
devicesListBinding.devicesList.addHeaderView(headerText);
devicesListBinding.refreshListLayout.setOnRefreshListener(this::refreshDevicesAction);
return devicesListBinding.getRoot();
}
@ -207,12 +227,17 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb
}
}
boolean hasNotificationsPermission = ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED;
devicesListBinding.devicesList.removeHeaderView(headerText);
devicesListBinding.devicesList.removeHeaderView(noWifiHeader);
devicesListBinding.devicesList.removeHeaderView(notTrustedText);
devicesListBinding.devicesList.removeHeaderView(noNotificationsHeader);
if (someDevicesReachable || isConnectedToNonCellularNetwork) {
if (TrustedNetworkHelper.isTrustedNetwork(getContext())) {
if (!hasNotificationsPermission) {
devicesListBinding.devicesList.addHeaderView(noNotificationsHeader);
} else if (TrustedNetworkHelper.isTrustedNetwork(getContext())) {
devicesListBinding.devicesList.addHeaderView(headerText);
} else {
devicesListBinding.devicesList.addHeaderView(notTrustedText);

View File

@ -11,6 +11,8 @@ import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import org.kde.kdeconnect_tp.R;
public class PermissionsAlertDialogFragment extends AlertDialogFragment {
private static final String KEY_PERMISSIONS = "Permissions";
private static final String KEY_REQUEST_CODE = "RequestCode";
@ -43,6 +45,12 @@ public class PermissionsAlertDialogFragment extends AlertDialogFragment {
}
public static class Builder extends AlertDialogFragment.AbstractBuilder<Builder, PermissionsAlertDialogFragment> {
public Builder() {
setPositiveButton(R.string.ok);
setNegativeButton(R.string.cancel);
}
@Override
public Builder getThis() {
return this;

View File

@ -75,8 +75,6 @@ public class TrustedNetworksActivity extends AppCompatActivity {
new PermissionsAlertDialogFragment.Builder()
.setTitle(R.string.location_permission_needed_title)
.setMessage(R.string.location_permission_needed_desc)
.setPositiveButton(R.string.ok)
.setNegativeButton(R.string.cancel)
.setPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION})
.setRequestCode(0)
.create().show(getSupportFragmentManager(), null);