Warning, /network/kdeconnect-android/src/org/kde/kdeconnect/UserInterface/MainActivity.kt is written in an unsupported language. File is not indexed.

0001 /*
0002  * SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 package org.kde.kdeconnect.UserInterface
0008 
0009 import android.Manifest
0010 import android.content.Intent
0011 import android.content.SharedPreferences
0012 import android.content.SharedPreferences.OnSharedPreferenceChangeListener
0013 import android.content.pm.PackageManager
0014 import android.os.Build
0015 import android.os.Bundle
0016 import android.util.Log
0017 import android.view.Menu
0018 import android.view.MenuItem
0019 import android.view.View
0020 import android.widget.ImageView
0021 import android.widget.TextView
0022 import androidx.activity.OnBackPressedCallback
0023 import androidx.appcompat.app.ActionBarDrawerToggle
0024 import androidx.appcompat.app.AppCompatActivity
0025 import androidx.core.app.ActivityCompat
0026 import androidx.core.content.ContextCompat
0027 import androidx.core.view.GravityCompat
0028 import androidx.drawerlayout.widget.DrawerLayout
0029 import androidx.fragment.app.Fragment
0030 import androidx.preference.PreferenceManager
0031 import com.google.android.material.navigation.NavigationView
0032 import org.apache.commons.lang3.ArrayUtils
0033 import org.kde.kdeconnect.BackgroundService
0034 import org.kde.kdeconnect.Device
0035 import org.kde.kdeconnect.Helpers.DeviceHelper
0036 import org.kde.kdeconnect.KdeConnect
0037 import org.kde.kdeconnect.Plugins.SharePlugin.ShareSettingsFragment
0038 import org.kde.kdeconnect.UserInterface.About.AboutFragment
0039 import org.kde.kdeconnect.UserInterface.About.getApplicationAboutData
0040 import org.kde.kdeconnect_tp.R
0041 import org.kde.kdeconnect_tp.databinding.ActivityMainBinding
0042 import java.util.LinkedList
0043 
0044 private const val MENU_ENTRY_ADD_DEVICE = 1 //0 means no-selection
0045 private const val MENU_ENTRY_SETTINGS = 2
0046 private const val MENU_ENTRY_ABOUT = 3
0047 private const val MENU_ENTRY_DEVICE_FIRST_ID = 1000 //All subsequent ids are devices in the menu
0048 private const val MENU_ENTRY_DEVICE_UNKNOWN = 9999 //It's still a device, but we don't know which one yet
0049 private const val STORAGE_LOCATION_CONFIGURED = 2020
0050 private const val STATE_SELECTED_MENU_ENTRY = "selected_entry" //Saved only in onSaveInstanceState
0051 private const val STATE_SELECTED_DEVICE = "selected_device" //Saved persistently in preferences
0052 
0053 class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
0054     private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
0055     private val mNavigationView: NavigationView by lazy { binding.navigationDrawer }
0056     private var mDrawerLayout: DrawerLayout? = null
0057 
0058     private lateinit var mNavViewDeviceName: TextView
0059 
0060     private var mCurrentDevice: String? = null
0061     private var mCurrentMenuEntry = 0
0062         private set(value) {
0063             field = value
0064             //Enabling "go to default fragment on back" callback when user in settings or "about" fragment
0065             mainFragmentCallback.isEnabled = value == MENU_ENTRY_SETTINGS || value == MENU_ENTRY_ABOUT
0066         }
0067     private val preferences: SharedPreferences by lazy { getSharedPreferences("stored_menu_selection", MODE_PRIVATE) }
0068     private val mMapMenuToDeviceId = HashMap<MenuItem, String>()
0069 
0070     private val closeDrawerCallback = object : OnBackPressedCallback(false) {
0071         override fun handleOnBackPressed() {
0072             mDrawerLayout?.closeDrawer(mNavigationView)
0073         }
0074     }
0075 
0076     private val mainFragmentCallback = object : OnBackPressedCallback(false) {
0077         override fun handleOnBackPressed() {
0078             mCurrentMenuEntry = mCurrentDevice?.let { deviceIdToMenuEntryId(it) } ?: MENU_ENTRY_ADD_DEVICE
0079             mNavigationView.setCheckedItem(mCurrentMenuEntry)
0080             setContentFragment(mCurrentDevice?.let { DeviceFragment.newInstance(it, false) } ?: PairingFragment())
0081         }
0082     }
0083 
0084     override fun onCreate(savedInstanceState: Bundle?) {
0085         super.onCreate(savedInstanceState)
0086         DeviceHelper.initializeDeviceId(this)
0087 
0088         val root = binding.root
0089         setContentView(root)
0090         mDrawerLayout = if (root is DrawerLayout) root else null
0091 
0092         val mDrawerHeader = mNavigationView.getHeaderView(0)
0093         mNavViewDeviceName = mDrawerHeader.findViewById(R.id.device_name)
0094         val mNavViewDeviceType = mDrawerHeader.findViewById<ImageView>(R.id.device_type)
0095 
0096         setSupportActionBar(binding.toolbarLayout.toolbar)
0097         mDrawerLayout?.let {
0098             supportActionBar?.setDisplayHomeAsUpEnabled(true)
0099             val mDrawerToggle = DrawerToggle(it).apply { syncState() }
0100             it.addDrawerListener(mDrawerToggle)
0101             it.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START)
0102         } ?: {
0103             supportActionBar?.setDisplayShowHomeEnabled(false)
0104             supportActionBar?.setHomeButtonEnabled(false)
0105         }
0106 
0107         // Note: The preference changed listener should be registered before getting the name, because getting
0108         // it can trigger a background fetch from the internet that will eventually update the preference
0109         PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this)
0110         val deviceName = DeviceHelper.getDeviceName(this)
0111         mNavViewDeviceType?.setImageDrawable(DeviceHelper.getDeviceType(this).getIcon(this))
0112         mNavViewDeviceName.text = deviceName
0113         mNavigationView.setNavigationItemSelectedListener { menuItem: MenuItem ->
0114             mCurrentMenuEntry = menuItem.itemId
0115             when (mCurrentMenuEntry) {
0116                 MENU_ENTRY_ADD_DEVICE -> {
0117                     mCurrentDevice = null
0118                     preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply()
0119                     setContentFragment(PairingFragment())
0120                 }
0121 
0122                 MENU_ENTRY_SETTINGS -> {
0123                     preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply()
0124                     setContentFragment(SettingsFragment())
0125                 }
0126 
0127                 MENU_ENTRY_ABOUT -> {
0128                     preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply()
0129                     setContentFragment(AboutFragment.newInstance(getApplicationAboutData(this)))
0130                 }
0131 
0132                 else -> {
0133                     val deviceId = mMapMenuToDeviceId[menuItem]
0134                     onDeviceSelected(deviceId)
0135                 }
0136             }
0137             mDrawerLayout?.closeDrawer(mNavigationView)
0138             true
0139         }
0140 
0141         // Decide which menu entry should be selected at start
0142         var savedDevice: String?
0143         var savedMenuEntry: Int
0144         when {
0145             intent.hasExtra(FLAG_FORCE_OVERVIEW) -> {
0146                 Log.i(this::class.simpleName, "Requested to start main overview")
0147                 savedDevice = null
0148                 savedMenuEntry = MENU_ENTRY_ADD_DEVICE
0149             }
0150 
0151             intent.hasExtra(EXTRA_DEVICE_ID) -> {
0152                 Log.i(this::class.simpleName, "Loading selected device from parameter")
0153                 savedDevice = intent.getStringExtra(EXTRA_DEVICE_ID)
0154                 savedMenuEntry = MENU_ENTRY_DEVICE_UNKNOWN
0155                 // If pairStatus is not empty, then the user has accepted/reject the pairing from the notification
0156                 val pairStatus = intent.getStringExtra(PAIR_REQUEST_STATUS)
0157                 if (pairStatus != null) {
0158                     Log.i(this::class.simpleName, "Pair status is $pairStatus")
0159                     savedDevice = onPairResultFromNotification(savedDevice, pairStatus)
0160                     if (savedDevice == null) {
0161                         savedMenuEntry = MENU_ENTRY_ADD_DEVICE
0162                     }
0163                 }
0164             }
0165 
0166             savedInstanceState != null -> {
0167                 Log.i(this::class.simpleName, "Loading selected device from saved activity state")
0168                 savedDevice = savedInstanceState.getString(STATE_SELECTED_DEVICE)
0169                 savedMenuEntry = savedInstanceState.getInt(STATE_SELECTED_MENU_ENTRY, MENU_ENTRY_ADD_DEVICE)
0170             }
0171 
0172             else -> {
0173                 Log.i(this::class.simpleName, "Loading selected device from persistent storage")
0174                 savedDevice = preferences.getString(STATE_SELECTED_DEVICE, null)
0175                 savedMenuEntry = if (savedDevice != null) MENU_ENTRY_DEVICE_UNKNOWN else MENU_ENTRY_ADD_DEVICE
0176             }
0177         }
0178         mCurrentMenuEntry = savedMenuEntry
0179         mCurrentDevice = savedDevice
0180         mNavigationView.setCheckedItem(savedMenuEntry)
0181 
0182         //FragmentManager will restore whatever fragment was there
0183         if (savedInstanceState != null) {
0184             val frag = supportFragmentManager.findFragmentById(R.id.container)
0185             if (frag !is DeviceFragment || frag.deviceId == savedDevice) return
0186         }
0187 
0188         // Activate the chosen fragment and select the entry in the menu
0189         if (savedMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID && savedDevice != null) {
0190             onDeviceSelected(savedDevice)
0191         } else {
0192             when (mCurrentMenuEntry) {
0193                 MENU_ENTRY_SETTINGS -> setContentFragment(SettingsFragment())
0194                 MENU_ENTRY_ABOUT -> setContentFragment(AboutFragment.newInstance(getApplicationAboutData(this)))
0195                 else -> setContentFragment(PairingFragment())
0196             }
0197         }
0198 
0199         val missingPermissions = mutableListOf<String>()
0200 
0201         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
0202             val permissionResult = ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
0203             if (permissionResult != PackageManager.PERMISSION_GRANTED) {
0204                 if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) {
0205                     missingPermissions.add(Manifest.permission.POST_NOTIFICATIONS)
0206                 }
0207             }
0208         }
0209 
0210         if(missingPermissions.size > 0){
0211             ActivityCompat.requestPermissions(this, missingPermissions.toTypedArray(), RESULT_NOTIFICATIONS_ENABLED)
0212         }
0213     }
0214 
0215     override fun onDestroy() {
0216         super.onDestroy()
0217         PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this)
0218     }
0219 
0220     private fun onPairResultFromNotification(deviceId: String?, pairStatus: String): String? {
0221         assert(deviceId != null)
0222         if (pairStatus != PAIRING_PENDING) {
0223             val device = KdeConnect.getInstance().getDevice(deviceId)
0224             if (device == null) {
0225                 Log.w(this::class.simpleName, "Reject pairing - device no longer exists: $deviceId")
0226                 return null
0227             }
0228             when (pairStatus) {
0229                 PAIRING_ACCEPTED -> device.acceptPairing()
0230                 PAIRING_REJECTED -> device.cancelPairing()
0231             }
0232         }
0233         return if (pairStatus == PAIRING_ACCEPTED || pairStatus == PAIRING_PENDING) deviceId else null
0234     }
0235 
0236     private fun deviceIdToMenuEntryId(deviceId: String?): Int {
0237         for ((key, value) in mMapMenuToDeviceId) {
0238             if (value == deviceId) {
0239                 return key.itemId
0240             }
0241         }
0242         return MENU_ENTRY_DEVICE_UNKNOWN
0243     }
0244 
0245     override fun onOptionsItemSelected(item: MenuItem): Boolean {
0246         return if (item.itemId == android.R.id.home) {
0247             mDrawerLayout?.openDrawer(mNavigationView)
0248             true
0249         } else {
0250             super.onOptionsItemSelected(item)
0251         }
0252     }
0253 
0254     private fun updateDeviceList() {
0255         val menu = mNavigationView.menu
0256         menu.clear()
0257         mMapMenuToDeviceId.clear()
0258         val devicesMenu = menu.addSubMenu(R.string.devices)
0259         var id = MENU_ENTRY_DEVICE_FIRST_ID
0260         val devices: Collection<Device> = KdeConnect.getInstance().devices.values
0261         for (device in devices) {
0262             if (device.isReachable && device.isPaired) {
0263                 val item = devicesMenu.add(Menu.FIRST, id++, 1, device.name)
0264                 item.icon = device.icon
0265                 item.isCheckable = true
0266                 mMapMenuToDeviceId[item] = device.deviceId
0267             }
0268         }
0269         val addDeviceItem = devicesMenu.add(Menu.FIRST, MENU_ENTRY_ADD_DEVICE, 1000, R.string.pair_new_device)
0270         addDeviceItem.setIcon(R.drawable.ic_action_content_add_circle_outline_32dp)
0271         addDeviceItem.isCheckable = true
0272         val settingsItem = menu.add(Menu.FIRST, MENU_ENTRY_SETTINGS, 1000, R.string.settings)
0273         settingsItem.setIcon(R.drawable.ic_settings_white_32dp)
0274         settingsItem.isCheckable = true
0275         val aboutItem = menu.add(Menu.FIRST, MENU_ENTRY_ABOUT, 1000, R.string.about)
0276         aboutItem.setIcon(R.drawable.ic_baseline_info_24)
0277         aboutItem.isCheckable = true
0278 
0279         //Ids might have changed
0280         if (mCurrentMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID) {
0281             mCurrentMenuEntry = deviceIdToMenuEntryId(mCurrentDevice)
0282         }
0283         mNavigationView.setCheckedItem(mCurrentMenuEntry)
0284     }
0285 
0286     override fun onStart() {
0287         super.onStart()
0288         BackgroundService.Start(applicationContext)
0289         KdeConnect.getInstance().addDeviceListChangedCallback(this::class.simpleName) { runOnUiThread { updateDeviceList() } }
0290         updateDeviceList()
0291         onBackPressedDispatcher.addCallback(mainFragmentCallback)
0292         onBackPressedDispatcher.addCallback(closeDrawerCallback)
0293         if (mDrawerLayout == null) closeDrawerCallback.isEnabled = false
0294     }
0295 
0296     override fun onStop() {
0297         KdeConnect.getInstance().removeDeviceListChangedCallback(this::class.simpleName)
0298         mainFragmentCallback.remove()
0299         closeDrawerCallback.remove()
0300         super.onStop()
0301     }
0302 
0303     @JvmOverloads
0304     fun onDeviceSelected(deviceId: String?, fromDeviceList: Boolean = false) {
0305         mCurrentDevice = deviceId
0306         preferences.edit().putString(STATE_SELECTED_DEVICE, deviceId).apply()
0307         if (mCurrentDevice != null) {
0308             mCurrentMenuEntry = deviceIdToMenuEntryId(deviceId)
0309             if (mCurrentMenuEntry == MENU_ENTRY_DEVICE_UNKNOWN) {
0310                 uncheckAllMenuItems(mNavigationView.menu)
0311             } else {
0312                 mNavigationView.setCheckedItem(mCurrentMenuEntry)
0313             }
0314             setContentFragment(DeviceFragment.newInstance(deviceId, fromDeviceList))
0315         } else {
0316             mCurrentMenuEntry = MENU_ENTRY_ADD_DEVICE
0317             mNavigationView.setCheckedItem(mCurrentMenuEntry)
0318             setContentFragment(PairingFragment())
0319         }
0320     }
0321 
0322     private fun setContentFragment(fragment: Fragment) {
0323         supportFragmentManager
0324             .beginTransaction()
0325             .replace(R.id.container, fragment)
0326             .commit()
0327     }
0328 
0329     override fun onSaveInstanceState(outState: Bundle) {
0330         super.onSaveInstanceState(outState)
0331         outState.putString(STATE_SELECTED_DEVICE, mCurrentDevice)
0332         outState.putInt(STATE_SELECTED_MENU_ENTRY, mCurrentMenuEntry)
0333     }
0334 
0335     @Deprecated("Deprecated in Java")
0336     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
0337         when {
0338             requestCode == RESULT_NEEDS_RELOAD -> {
0339                 KdeConnect.getInstance().devices.values.forEach(Device::reloadPluginsFromSettings)
0340             }
0341             requestCode == STORAGE_LOCATION_CONFIGURED && resultCode == RESULT_OK && data != null -> {
0342                 val uri = data.data
0343                 ShareSettingsFragment.saveStorageLocationPreference(this, uri)
0344             }
0345             else -> super.onActivityResult(requestCode, resultCode, data)
0346         }
0347     }
0348 
0349     fun isPermissionGranted(permissions: Array<String>, grantResults: IntArray, permission : String) : Boolean {
0350         val index = ArrayUtils.indexOf(permissions, permission)
0351         return index != ArrayUtils.INDEX_NOT_FOUND && grantResults[index] == PackageManager.PERMISSION_GRANTED
0352     }
0353 
0354     override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
0355         super.onRequestPermissionsResult(requestCode, permissions, grantResults)
0356         val permissionsGranted = ArrayUtils.contains(grantResults, PackageManager.PERMISSION_GRANTED)
0357         if (permissionsGranted) {
0358             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isPermissionGranted(permissions, grantResults, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
0359                 // To get a writeable path manually on Android 10 and later for Share and Receive Plugin.
0360                 // Otherwise, Receiving files will keep failing until the user chooses a path manually to receive files.
0361                 val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
0362                 startActivityForResult(intent, STORAGE_LOCATION_CONFIGURED)
0363             }
0364 
0365             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isPermissionGranted(permissions, grantResults, Manifest.permission.POST_NOTIFICATIONS)) {
0366                 // If PairingFragment is active, reload it
0367                 if (mCurrentDevice == null) {
0368                     setContentFragment(PairingFragment())
0369                 }
0370             }
0371 
0372             //New permission granted, reload plugins
0373             KdeConnect.getInstance().devices.values.forEach(Device::reloadPluginsFromSettings)
0374         }
0375     }
0376 
0377     override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
0378         if (DeviceHelper.KEY_DEVICE_NAME_PREFERENCE == key) {
0379             mNavViewDeviceName.text = DeviceHelper.getDeviceName(this)
0380             BackgroundService.ForceRefreshConnections(this) //Re-send our identity packet
0381         }
0382     }
0383 
0384     private fun uncheckAllMenuItems(menu: Menu) {
0385         val size = menu.size()
0386         for (i in 0 until size) {
0387             val item = menu.getItem(i)
0388             item.subMenu?.let { uncheckAllMenuItems(it) } ?: item.setChecked(false)
0389         }
0390     }
0391 
0392     companion object {
0393         const val EXTRA_DEVICE_ID = "deviceId"
0394         const val PAIR_REQUEST_STATUS = "pair_req_status"
0395         const val PAIRING_ACCEPTED = "accepted"
0396         const val PAIRING_REJECTED = "rejected"
0397         const val PAIRING_PENDING = "pending"
0398         const val RESULT_NEEDS_RELOAD = RESULT_FIRST_USER
0399         const val RESULT_NOTIFICATIONS_ENABLED = RESULT_FIRST_USER+1
0400         const val FLAG_FORCE_OVERVIEW = "forceOverview"
0401     }
0402 
0403     private inner class DrawerToggle(drawerLayout: DrawerLayout) : ActionBarDrawerToggle(
0404         this,  /* host Activity */
0405         drawerLayout,  /* DrawerLayout object */
0406         R.string.open,  /* "open drawer" description */
0407         R.string.close /* "close drawer" description */
0408     ) {
0409         override fun onDrawerClosed(drawerView: View) {
0410             super.onDrawerClosed(drawerView)
0411             closeDrawerCallback.isEnabled = false
0412         }
0413 
0414         override fun onDrawerOpened(drawerView: View) {
0415             super.onDrawerOpened(drawerView)
0416             closeDrawerCallback.isEnabled = true
0417         }
0418     }
0419 
0420 }