ProximityManager.kt 13.8 KB
Newer Older
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
1
2
3
4
5
6
7
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 *
 * Authors
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
8
 * Created by Lunabee Studio / Date - 2020/13/05 - for the TOUS-ANTI-COVID project
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
9
10
11
12
13
14
 */

package com.lunabeestudio.stopcovid.manager

import android.Manifest
import android.annotation.SuppressLint
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
15
import android.app.NotificationManager
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
16
import android.bluetooth.BluetoothAdapter
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
17
import android.bluetooth.BluetoothManager
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
18
19
20
21
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
22
import android.location.LocationManager
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
23
24
25
26
27
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import android.view.View
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
28
import androidx.activity.result.ActivityResultLauncher
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
29
30
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
31
import androidx.core.location.LocationManagerCompat
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
32
33
import androidx.fragment.app.Fragment
import com.lunabeestudio.robert.RobertManager
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
34
import com.lunabeestudio.stopcovid.R
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
35
import com.lunabeestudio.stopcovid.coreui.UiConstants
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
36
import com.lunabeestudio.stopcovid.coreui.extension.openAppSettings
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
37
import com.lunabeestudio.stopcovid.model.CovidException
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
38
import com.lunabeestudio.stopcovid.model.DeviceSetup
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
39
import com.lunabeestudio.stopcovid.widgetshomescreen.ProximityWidget
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
40
41
42

object ProximityManager {

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
43
44
45
46
    fun isProximityOn(context: Context, robertManager: RobertManager): Boolean {
        ProximityWidget.updateWidget(context)
        return robertManager.isProximityActive && getDeviceSetup(context, robertManager) == DeviceSetup.BLE
    }
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
47

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
48
    fun getDeviceSetup(context: Context, robertManager: RobertManager): DeviceSetup = when {
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
49
50
51
52
53
54
55
        isNotificationOn(context) &&
            isProximityGranted(context) &&
            hasFeatureBLE(context, robertManager) &&
            isBluetoothOn(context, robertManager) &&
            isBatteryOptimizationOff(context) &&
            isAdvertisingValid(robertManager, context) &&
            !needLocalisationTurnedOn(context) -> DeviceSetup.BLE
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
56
        !hasFeatureBLE(context, robertManager) -> DeviceSetup.NO_BLE
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
57
58
        else -> DeviceSetup.NOT_SETUP
    }
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
59

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
60
    fun hasFeatureBLE(context: Context, robertManager: RobertManager): Boolean {
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
61
        val hasBLESystemFeature = context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
62
        val isDeviceSupported = robertManager.configuration.unsupportedDevices?.contains(Build.MODEL) != true
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
63
64
65
        return isDeviceSupported && hasBLESystemFeature
    }

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
66
    fun isAdvertisingValid(robertManager: RobertManager, context: Context): Boolean {
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
67
68
        return getBluetoothAdapter(context)?.bluetoothLeAdvertiser != null ||
            robertManager.configuration.allowNoAdvertisingDevice
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
69
70
    }

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
71
72
73
74
75
    private fun getBluetoothAdapter(context: Context) =
        (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter

    fun hasUnstableBluetooth(context: Context): Boolean {
        return getBluetoothAdapter(context)?.bluetoothLeAdvertiser == null && Build.VERSION.SDK_INT < Build.VERSION_CODES.N
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
76
    }
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
77

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
78
    private fun isNotificationOn(context: Context): Boolean =
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
79
80
81
82
83
84
85
86
87
88
89
        NotificationManagerCompat.from(context).areNotificationsEnabled() && isProximityNotificationChannelEnabled(context)

    private fun isProximityNotificationChannelEnabled(context: Context): Boolean {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            val channel = manager.getNotificationChannel(UiConstants.Notification.PROXIMITY.channelId)
            channel?.importance != NotificationManager.IMPORTANCE_NONE
        } else {
            true
        }
    }
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
90

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
91
92
93
94
95
96
97
98
99
100
101
102
103
104
    fun isProximityGranted(context: Context): Boolean =
        getManifestProximityPermissions().all { permission ->
            ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
        }

    fun getManifestProximityPermissions(): Array<String> = when {
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> arrayOf(
            Manifest.permission.BLUETOOTH_CONNECT,
            Manifest.permission.BLUETOOTH_SCAN,
            Manifest.permission.BLUETOOTH_ADVERTISE
        )
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
        else -> arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION)
    }
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
105

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
106
107
108
109
    fun isLocationRequired(): Boolean {
        val manifestProximityPermissions = getManifestProximityPermissions()
        return manifestProximityPermissions.contains(Manifest.permission.ACCESS_FINE_LOCATION) ||
            manifestProximityPermissions.contains(Manifest.permission.ACCESS_COARSE_LOCATION)
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
110
111
    }

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
112
    private fun needLocalisationTurnedOn(context: Context): Boolean {
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
113
114
115
        return if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&
            getBluetoothAdapter(context)?.isOffloadedScanBatchingSupported != true
        ) {
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
116
117
118
119
120
121
            !LocationManagerCompat.isLocationEnabled(context.getSystemService(Context.LOCATION_SERVICE) as LocationManager)
        } else {
            false
        }
    }

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
122
123
124
    fun isBluetoothOn(context: Context, robertManager: RobertManager): Boolean = hasFeatureBLE(
        context,
        robertManager
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
125
    ) && getBluetoothAdapter(context)?.isEnabled != false
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
126

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
127
    fun isBatteryOptimizationOff(context: Context): Boolean {
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
128
        val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
129
130
131
        return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
            pm?.isIgnoringBatteryOptimizations(context.packageName) == true ||
            !hasActivityToResolveIgnoreBatteryOptimization(context)
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
132
133
    }

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
134
135
136
    fun getErrorClickListener(
        fragment: Fragment,
        robertManager: RobertManager,
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
137
        activityResultLauncher: ActivityResultLauncher<Intent>?,
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
138
        serviceError: CovidException?,
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
139
        nearbyDevicePermissionsResultLauncher: ActivityResultLauncher<Array<String>>?,
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
140
141
142
143
        activateProximity: () -> Unit,
        restartProximity: () -> Unit,
    ): View.OnClickListener? = when {
        !hasFeatureBLE(fragment.requireContext(), robertManager) -> null
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
144
145
146
147
        !isNotificationOn(fragment.requireContext()) -> View.OnClickListener {
            fragment.openAppSettings()
        }
        !isProximityGranted(fragment.requireContext()) -> View.OnClickListener {
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
148
149
150
151
152
            if (nearbyDevicePermissionsResultLauncher != null) {
                nearbyDevicePermissionsResultLauncher.launch(getManifestProximityPermissions())
            } else {
                fragment.openAppSettings()
            }
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
153
        }
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
154
155
156
157
        hasFeatureBLE(fragment.requireContext(), robertManager) && !isBluetoothOn(
            fragment.requireContext(),
            robertManager
        ) -> View.OnClickListener {
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
158
159
160
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            fragment.startActivity(enableBtIntent)
        }
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
161
        !isAdvertisingValid(robertManager, fragment.requireContext()) -> null
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
162
        !isBatteryOptimizationOff(fragment.requireContext()) -> View.OnClickListener {
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
163
            requestIgnoreBatteryOptimization(fragment, activityResultLauncher)
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
164
        }
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
165
166
167
        needLocalisationTurnedOn(fragment.requireContext()) -> View.OnClickListener {
            fragment.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
        }
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
168
169
170
        serviceError != null -> View.OnClickListener {
            restartProximity()
        }
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
171
172
173
174
175
176
177
178
        else -> View.OnClickListener {
            activateProximity()
        }
    }

    private val powerManagerIntents = arrayOf(
        Intent().setComponent(ComponentName("com.miui.securitycenter", "com.miui.permcenter.autostart.AutoStartManagementActivity")),
        Intent().setComponent(ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.AutobootManageActivity")),
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
179
180
181
182
183
184
        Intent().setComponent(
            ComponentName(
                "com.huawei.systemmanager",
                "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity"
            )
        ),
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
185
        Intent().setComponent(ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.optimize.process.ProtectActivity")),
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
186
187
188
189
190
191
        Intent().setComponent(
            ComponentName(
                "com.huawei.systemmanager",
                "com.huawei.systemmanager.appcontrol.activity.StartupAppControlActivity"
            )
        ),
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
192
193
194
195
196
197
198
199
200
201
202
        Intent().setComponent(ComponentName("com.coloros.safecenter", "com.coloros.safecenter.permission.startup.StartupAppListActivity")),
        Intent().setComponent(ComponentName("com.coloros.safecenter", "com.coloros.safecenter.startupapp.StartupAppListActivity")),
        Intent().setComponent(ComponentName("com.oppo.safe", "com.oppo.safe.permission.startup.StartupAppListActivity")),
        Intent().setComponent(ComponentName("com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.AddWhiteListActivity")),
        Intent().setComponent(ComponentName("com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.BgStartUpManager")),
        Intent().setComponent(ComponentName("com.vivo.permissionmanager", "com.vivo.permissionmanager.activity.BgStartUpManagerActivity")),
        Intent().setComponent(ComponentName("com.samsung.android.lool", "com.samsung.android.sm.ui.battery.BatteryActivity")),
        Intent().setComponent(ComponentName("com.htc.pitroad", "com.htc.pitroad.landingpage.activity.LandingPageActivity")),
        Intent().setComponent(ComponentName("com.asus.mobilemanager", "com.asus.mobilemanager.MainActivity"))
    )

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
203
204
205
206
207
    @SuppressLint("InlinedApi")
    private fun getIgnoreBatteryOptimizationIntents(context: Context): List<Intent> {
        val miuiIntent = Intent("miui.intent.action.HIDDEN_APPS_CONFIG_ACTIVITY")
        miuiIntent.putExtra("package_name", context.packageName)
        miuiIntent.putExtra("package_label", context.getString(R.string.app_name))
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
208
209
        val systemIntent = Intent()
        systemIntent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
210
211
        systemIntent.data = Uri.parse("package:${context.packageName}")
        val powerIntents = arrayListOf(miuiIntent, systemIntent)
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
212
        powerIntents.addAll(powerManagerIntents)
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
213
214
215
216
217
        return powerIntents
    }

    fun requestIgnoreBatteryOptimization(fragment: Fragment, activityResultLauncher: ActivityResultLauncher<Intent>?) {
        val powerIntents = getIgnoreBatteryOptimizationIntents(fragment.requireContext())
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
218
219
220
        for (intent in powerIntents) {
            val resolveInfo = intent.resolveActivityInfo(fragment.requireContext().packageManager, PackageManager.MATCH_DEFAULT_ONLY)
            if (resolveInfo?.exported == true) {
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
221
                activityResultLauncher?.launch(intent)
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
222
223
224
225
226
227
                break
            }
        }
    }

    private fun hasActivityToResolveIgnoreBatteryOptimization(context: Context): Boolean {
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
228
        val powerIntents = getIgnoreBatteryOptimizationIntents(context)
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
229
        for (intent in powerIntents) {
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
230
231
232
233
            intent.apply {
                putExtra("package_name", context.packageName)
                putExtra("package_label", context.getString(R.string.app_name))
            }
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
234
235
236
237
238
239
240
241
            val resolveInfo = intent.resolveActivityInfo(context.packageManager, PackageManager.MATCH_DEFAULT_ONLY)
            if (resolveInfo?.exported == true) {
                return true
            }
        }
        return false
    }

stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
242
243
244
245
246
247
248
    fun getErrorText(
        fragment: Fragment,
        robertManager: RobertManager,
        serviceError: CovidException?,
        strings: Map<String, String>,
    ): String? = when {
        !hasFeatureBLE(fragment.requireContext(), robertManager) -> strings["proximityController.error.noBLE"]
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
249
250
251
        !isNotificationOn(fragment.requireContext()) &&
            !isBluetoothOn(fragment.requireContext(), robertManager) &&
            !isProximityGranted(fragment.requireContext()) ->
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
252
            strings["proximityController.error.noNotificationsOrBluetoothOrLocalisation"]
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
253
        !isBluetoothOn(fragment.requireContext(), robertManager) && !isProximityGranted(fragment.requireContext()) ->
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
254
            strings["proximityController.error.noBluetoothOrLocalisation"]
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
255
        !isNotificationOn(fragment.requireContext()) && !isProximityGranted(fragment.requireContext()) ->
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
256
            strings["proximityController.error.noNotificationsOrLocalisation"]
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
257
258
259
260
        !isNotificationOn(fragment.requireContext()) && !isBluetoothOn(
            fragment.requireContext(),
            robertManager
        ) -> strings["proximityController.error.noNotificationsOrBluetooth"]
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
261
        !isNotificationOn(fragment.requireContext()) -> strings["proximityController.error.noNotifications"]
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
262
263
264
265
266
        !isProximityGranted(fragment.requireContext()) -> if (isLocationRequired()) {
            strings["proximityController.error.noLocalisation"]
        } else {
            strings["proximityController.error.noNearbyDevicesAccess"]
        }
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
267
        !isBluetoothOn(fragment.requireContext(), robertManager) -> strings["proximityController.error.noBluetooth"]
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
268
        !isAdvertisingValid(robertManager, fragment.requireContext()) -> strings["proximityController.error.noAdvertising"]
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
269
270
        !isBatteryOptimizationOff(fragment.requireContext()) -> strings["proximityController.error.noBattery"]
        needLocalisationTurnedOn(fragment.requireContext()) -> strings["proximityController.error.batchLocalisation"]
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
271
        !robertManager.isProximityActive -> strings["proximityController.error.activateProximity"]
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
272
        serviceError != null -> strings["common.error.bleScanner"]
stopcovid@lunabee.com's avatar
Unzip    
stopcovid@lunabee.com committed
273
274
        else -> null
    }
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
275
276
277
278
279
280
281
282

    fun getProximityPermissionExplanationKey(): String {
        return if (isLocationRequired()) {
            "common.needLocalisationAccessToScan"
        } else {
            "common.needNearbyDevicesAccessToScan"
        }
    }
stopcovid@lunabee.com's avatar
stopcovid@lunabee.com committed
283
}