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 }