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

Main activity responsive layout

This commit is contained in:
Dmitry Yudin 2023-04-07 00:05:05 +02:00
parent e5f221f891
commit 6e9cbfb030
No known key found for this signature in database
20 changed files with 636 additions and 572 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

BIN
.idea/icon.png generated Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -4,7 +4,7 @@ import com.android.build.gradle.api.ApplicationVariant
import com.github.jk1.license.render.TextReportRenderer
buildscript {
ext.kotlin_version = '1.8.0'
ext.kotlin_version = '1.8.10'
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

View File

@ -0,0 +1,30 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigation_drawer"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/nav_header" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout"
android:layout_height="match_parent"
android:layout_width="match_parent"
tools:context="org.kde.kdeconnect.UserInterface.MainActivity">
<include layout="@layout/toolbar" android:id="@+id/toolbar_layout" />
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View File

@ -1,44 +1,51 @@
<LinearLayout
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="@integer/activity_device_orientation"
tools:context="org.kde.kdeconnect.UserInterface.DeviceFragment">
android:layout_height="match_parent">
<!-- Layout shown when device is reachable but not yet paired -->
<include
android:id="@+id/pair_request"
layout="@layout/view_pair_request"
tools:visibility="gone"/>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<!-- Layout shown when we can't pair with device or device is not reachable -->
<include
android:id="@+id/pair_error"
layout="@layout/view_pair_error"
tools:visibility="gone"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:context="org.kde.kdeconnect.UserInterface.DeviceFragment">
<!-- Layouts shown when device is paired and reachable -->
<GridView
android:id="@+id/plugins_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="@integer/plugins_list_weight"
android:numColumns="@integer/plugins_columns"
android:horizontalSpacing="8dp"
android:verticalSpacing="8dp"
android:layout_margin="@dimen/activity_vertical_margin"
tools:listitem="@layout/list_plugin_entry"
tools:layout_height="300dp" />
<!-- Layout shown when device is reachable but not yet paired -->
<include
android:id="@+id/pair_request"
layout="@layout/view_pair_request"
tools:visibility="gone"/>
<ListView
android:id="@+id/buttons_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="@integer/buttons_list_weight"
android:divider="@null"
android:dividerHeight="0dp"
tools:context=".DeviceActivity"
tools:listitem="@layout/list_item_with_icon_entry"
tools:layout_height="300dp" />
</LinearLayout>
<!-- Layout shown when we can't pair with device or device is not reachable -->
<include
android:id="@+id/pair_error"
layout="@layout/view_pair_error"
tools:visibility="gone"/>
<!-- Layouts shown when device is paired and reachable -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/plugins_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:nestedScrollingEnabled="false"
tools:listitem="@layout/list_plugin_entry"
tools:layout_height="300dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/buttons_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
tools:context=".DeviceActivity"
tools:listitem="@layout/list_item_plugin_header"
tools:layout_height="300dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -1,32 +1,37 @@
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"> <!-- fitSystemWindows to make the drawer slide below the Lollipop transparent status bar -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout"
android:layout_height="match_parent"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
tools:context="org.kde.kdeconnect.UserInterface.MainActivity">
android:layout_height="match_parent">
<include layout="@layout/toolbar" android:id="@+id/toolbar_layout" />
<FrameLayout
android:id="@+id/container"
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
android:fitsSystemWindows="true"> <!-- fitSystemWindows to make the drawer slide below the Lollipop transparent status bar -->
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout"
android:layout_height="match_parent"
android:layout_width="match_parent"
tools:context="org.kde.kdeconnect.UserInterface.MainActivity">
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigation_drawer"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/nav_header" />
<include layout="@layout/toolbar" android:id="@+id/toolbar_layout"/>
</androidx.drawerlayout.widget.DrawerLayout>
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigation_drawer"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/nav_header"/>
</androidx.drawerlayout.widget.DrawerLayout>
</FrameLayout>

View File

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="16dp"
android:paddingTop="28dp"
android:paddingRight="16dp"
android:paddingBottom="8dp" />
android:maxWidth="400dp"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:paddingHorizontal="@dimen/activity_horizontal_margin"
android:paddingVertical="@dimen/activity_vertical_margin"
tools:background="@android:color/darker_gray"
tools:text="@tools:sample/lorem"/>

View File

@ -2,8 +2,9 @@
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
style="@style/KdeConnectCardStyle.Filled"
app:contentPadding="12dp"
tools:layout_width="240dp">
@ -17,7 +18,7 @@
android:minHeight="?android:attr/listPreferredItemHeight"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
<ImageView
android:id="@+id/list_item_entry_icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"

View File

@ -33,17 +33,14 @@
android:drawablePadding="5dp"
android:layout_marginBottom="8dip"
android:visibility="gone"
android:text=""
android:textAppearance="?android:attr/textAppearanceMedium"
app:drawableStartCompat="@drawable/ic_key" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/pair_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/button_round"
android:text="@string/request_pairing"
android:textColor="@android:color/white" />
android:text="@string/request_pairing" />
<LinearLayout
@ -51,27 +48,27 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone">
android:visibility="gone"
android:paddingVertical="4dp"
tools:visibility="visible">
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/accept_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="4dip"
android:layout_weight="1"
android:background="@drawable/button_round"
android:text="@string/pairing_accept"
android:textColor="@android:color/white" />
android:text="@string/pairing_accept" />
<Button
<android.widget.Space
android:layout_width="8dp"
android:layout_height="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/reject_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="4dip"
android:layout_weight="1"
android:background="@drawable/button_round"
android:text="@string/pairing_reject"
android:textColor="@android:color/white" />
android:text="@string/pairing_reject" />
</LinearLayout>
</LinearLayout>

View File

@ -1,11 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="activity_device_orientation">@integer/orientation_horizontal</integer>
<integer name="plugins_list_weight">4</integer>
<integer name="buttons_list_weight">6</integer>
<integer name="mpris_now_playing_orientation">@integer/orientation_horizontal</integer>
<integer name="mpris_now_playing_album_weight">1</integer>
<integer name="mpris_now_playing_controls_weight">1</integer>

View File

@ -10,9 +10,6 @@
<item name="layout_wrap_content" type="dimen">-2</item>
<!--used in activity_device-->
<integer name="activity_device_orientation">@integer/orientation_vertical</integer>
<integer name="plugins_list_weight">@null</integer>
<integer name="buttons_list_weight">@null</integer>
<integer name="plugins_columns">2</integer>
<!--used in mpris_now_playing-->

View File

@ -162,7 +162,7 @@ public class RemoteKeyboardService
}
} else { // != 1 instance of plugin -> show main activity view
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("forceOverview", true);
intent.putExtra(MainActivity.FLAG_FORCE_OVERVIEW, true);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
if (instances.size() < 1)

View File

@ -0,0 +1,15 @@
package org.kde.kdeconnect.Plugins
class StubTextPlugin(private val description: String) : Plugin() {
override fun getDisplayName() = description
override fun getDescription() = description
override fun getSupportedPacketTypes(): Array<String> {
throw UnsupportedOperationException("StubTextPlugin is used only with displayName and description")
}
override fun getOutgoingPacketTypes(): Array<String> {
throw UnsupportedOperationException("StubTextPlugin is used only with displayName and description")
}
}

View File

@ -32,13 +32,11 @@ internal class SystemVolumeProvider private constructor(plugin: SystemVolumePlug
@JvmStatic
fun fromPlugin(systemVolumePlugin: SystemVolumePlugin): SystemVolumeProvider {
if (currentProvider == null) {
currentProvider = SystemVolumeProvider(systemVolumePlugin)
}
val currentProvider = currentProvider ?: SystemVolumeProvider(systemVolumePlugin)
currentProvider!!.update(systemVolumePlugin)
currentProvider.update(systemVolumePlugin)
return currentProvider!!
return currentProvider
}
private fun scale(value: Int, maxValue: Int, maxScaled: Int): Int {

View File

@ -41,12 +41,10 @@ class AboutData(var name: String, var description: Int, var icon: Int, var versi
parcel.writeString(sourceCodeURL)
parcel.writeString(donateURL)
if (authorsFooterText == null) {
parcel.writeByte(0x00)
} else {
authorsFooterText?.let {
parcel.writeByte(0x01)
parcel.writeInt(authorsFooterText!!)
}
parcel.writeInt(it)
} ?: parcel.writeByte(0x00)
}
override fun describeContents(): Int = 0

View File

@ -6,7 +6,6 @@
package org.kde.kdeconnect.UserInterface
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
@ -14,10 +13,10 @@ import android.view.LayoutInflater
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.annotation.StringRes
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.kde.kdeconnect.BackgroundService
import org.kde.kdeconnect.Device
@ -26,10 +25,8 @@ import org.kde.kdeconnect.Device.PluginsChangedListener
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper
import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin
import org.kde.kdeconnect.Plugins.Plugin
import org.kde.kdeconnect.UserInterface.List.FailedPluginListItem
import org.kde.kdeconnect.UserInterface.List.ListAdapter
import org.kde.kdeconnect.UserInterface.List.PluginItem
import org.kde.kdeconnect.UserInterface.List.PluginListHeaderItem
import org.kde.kdeconnect.Plugins.StubTextPlugin
import org.kde.kdeconnect.UserInterface.List.PluginAdapter
import org.kde.kdeconnect_tp.R
import org.kde.kdeconnect_tp.databinding.ActivityDeviceBinding
import org.kde.kdeconnect_tp.databinding.ViewPairErrorBinding
@ -48,8 +45,8 @@ class DeviceFragment : Fragment() {
private val mActivity: MainActivity? by lazy { activity as MainActivity? }
//TODO use LinkedHashMap and delete irrelevant records when plugins changed
private val pluginListItems: ArrayList<ListAdapter.Item> = ArrayList()
private val permissionListItems: ArrayList<ListAdapter.Item> = ArrayList()
private val pluginListItems: ArrayList<Pair<Plugin, (() -> Unit)?>> = ArrayList()
private val permissionListItems: ArrayList<Pair<Plugin, (() -> Unit)?>> = ArrayList()
/**
* Top-level ViewBinding for this fragment.
@ -106,11 +103,13 @@ class DeviceFragment : Fragment() {
}
requireBinding().pairButton.setOnClickListener {
requireBinding().pairButton.visibility = View.GONE
requireBinding().pairMessage.text = null
requireBinding().pairVerification.visibility = View.VISIBLE
requireBinding().pairVerification.text = SslHelper.getVerificationKey(SslHelper.certificate, device?.certificate)
requireBinding().pairProgress.visibility = View.VISIBLE
with(requireBinding()) {
pairButton.visibility = View.GONE
pairMessage.text = null
pairVerification.visibility = View.VISIBLE
pairVerification.text = SslHelper.getVerificationKey(SslHelper.certificate, device?.certificate)
pairProgress.visibility = View.VISIBLE
}
device?.requestPairing()
}
requireBinding().acceptButton.setOnClickListener {
@ -141,6 +140,10 @@ class DeviceFragment : Fragment() {
refreshUI()
}
requireDeviceBinding().pluginsList.layoutManager =
GridLayoutManager(requireContext(), resources.getInteger(R.integer.plugins_columns))
requireDeviceBinding().buttonsList.layoutManager = LinearLayoutManager(requireContext())
return deviceBinding.root
}
@ -264,37 +267,41 @@ class DeviceFragment : Fragment() {
if (paired && reachable) {
//Plugins button list
val plugins: Collection<Plugin> = device.loadedPlugins.values
//TODO look for LinkedHashMap mention above
pluginListItems.clear()
permissionListItems.clear()
//Fill enabled plugins ArrayList
for (p in plugins) {
if (!p.hasMainActivity(context) || p.displayInContextMenu()) continue
pluginListItems.add(PluginItem(p) { p.startMainActivity(mActivity) })
pluginListItems.add(p to { p.startMainActivity(mActivity) })
}
//Fill permissionListItems with permissions plugins
createPermissionsList(
device.pluginsWithoutPermissions,
R.string.plugins_need_permission
) { plugin: Plugin ->
val dialog = plugin.permissionExplanationDialog
dialog?.show(childFragmentManager, null)
) { p: Plugin ->
p.permissionExplanationDialog?.show(childFragmentManager, null)
}
createPermissionsList(
device.pluginsWithoutOptionalPermissions,
R.string.plugins_need_optional_permission
) { plugin: Plugin ->
val dialog: DialogFragment? = plugin.optionalPermissionExplanationDialog
dialog?.show(childFragmentManager, null)
) { p: Plugin ->
p.optionalPermissionExplanationDialog?.show(childFragmentManager, null)
}
requireDeviceBinding().buttonsList.adapter =
PluginAdapter(permissionListItems, R.layout.list_item_plugin_header)
requireDeviceBinding().pluginsList.adapter =
PluginAdapter(pluginListItems, R.layout.list_plugin_entry)
requireDeviceBinding().pluginsList.adapter?.notifyDataSetChanged()
displayBatteryInfoIfPossible()
}
requireDeviceBinding().pluginsList.adapter = ListAdapter(mActivity, pluginListItems)
//don't do unnecessary work when all permissions granted and remove view for landscape orientation
if (permissionListItems.isEmpty()) {
requireDeviceBinding().buttonsList.visibility = View.GONE
} else {
requireDeviceBinding().buttonsList.adapter = ListAdapter(mActivity, permissionListItems)
requireDeviceBinding().buttonsList.visibility = View.VISIBLE
}
mActivity?.invalidateOptionsMenu()
} catch (e: IllegalStateException) {
//Ignore: The activity was closed while we were trying to update it
@ -320,7 +327,7 @@ class DeviceFragment : Fragment() {
mActivity?.runOnUiThread {
with(requireBinding()) {
pairMessage.text = error
pairVerification.text = ""
pairVerification.text = null
pairVerification.visibility = View.GONE
pairProgress.visibility = View.GONE
pairButton.visibility = View.VISIBLE
@ -346,17 +353,16 @@ class DeviceFragment : Fragment() {
private fun createPermissionsList(
plugins: ConcurrentHashMap<String, Plugin>,
headerText: Int,
action: FailedPluginListItem.Action
@StringRes headerText: Int,
action: (Plugin) -> Unit,
) {
if (plugins.isEmpty()) return
val device = device ?: return
permissionListItems.add(PluginListHeaderItem(headerText))
permissionListItems.add(StubTextPlugin(requireContext().getString(headerText)) to null)
for (plugin in plugins.values) {
if (!device.isPluginEnabled(plugin.pluginKey)) {
continue
if (device.isPluginEnabled(plugin.pluginKey)) {
permissionListItems.add(plugin to { action(plugin) })
}
permissionListItems.add(FailedPluginListItem(plugin, action))
}
}
@ -375,12 +381,10 @@ class DeviceFragment : Fragment() {
if (info != null) {
@StringRes
val resId: Int = if (info.isCharging) {
R.string.battery_status_charging_format
} else if (BatteryPlugin.isLowBattery(info)) {
R.string.battery_status_low_format
} else {
R.string.battery_status_format
val resId = when {
info.isCharging -> R.string.battery_status_charging_format
BatteryPlugin.isLowBattery(info) -> R.string.battery_status_low_format
else -> R.string.battery_status_format
}
mActivity?.supportActionBar?.subtitle = mActivity?.getString(resId, info.currentCharge)

View File

@ -0,0 +1,55 @@
package org.kde.kdeconnect.UserInterface.List
import android.annotation.TargetApi
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import org.kde.kdeconnect.Plugins.Plugin
import org.kde.kdeconnect.Plugins.StubTextPlugin
import org.kde.kdeconnect_tp.R
/**
* Adapter for showing enabled plugins and permission requests
* can be used with following layouts:
* list_plugin_entry - card view with text and icon
* list_item_plugin_header - plain TextView
* Any other TextView layout
*/
class PluginAdapter(
private val pluginList: ArrayList<Pair<Plugin, (() -> Unit)?>>,
private val layout: Int,
) : RecyclerView.Adapter<PluginAdapter.PluginViewHolder>() {
override fun onCreateViewHolder(viewGroup: ViewGroup, type: Int) =
PluginViewHolder(LayoutInflater.from(viewGroup.context).inflate(layout, viewGroup, false))
override fun getItemCount() = pluginList.size
@TargetApi(Build.VERSION_CODES.M)
override fun onBindViewHolder(holder: PluginViewHolder, position: Int) {
pluginList[position].let { (plugin, action) ->
holder.pluginTitle.text = plugin.displayName
holder.pluginIcon?.setImageDrawable(plugin.icon)
//Set regular text for unclickable StubTextPlugin and bold for supposedly clickable TextView items
when {
plugin is StubTextPlugin ->
holder.pluginTitle.setTextAppearance(R.style.TextAppearance_Material3_BodyMedium)
holder.itemView is TextView ->
holder.pluginTitle.setTextAppearance(R.style.TextAppearance_Material3_LabelLarge)
}
action?.let { holder.itemView.setOnClickListener { action.invoke() } }
}
}
class PluginViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val pluginTitle: TextView = view.findViewById(R.id.list_item_entry_title) ?: view as TextView
val pluginIcon: ImageView? = view.findViewById(R.id.list_item_entry_icon)
}
}

View File

@ -1,424 +0,0 @@
package org.kde.kdeconnect.UserInterface;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import com.google.android.material.navigation.NavigationView;
import org.apache.commons.lang3.ArrayUtils;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Plugins.SharePlugin.ShareSettingsFragment;
import org.kde.kdeconnect.UserInterface.About.AboutFragment;
import org.kde.kdeconnect.UserInterface.About.ApplicationAboutDataKt;
import org.kde.kdeconnect_tp.R;
import org.kde.kdeconnect_tp.databinding.ActivityMainBinding;
import java.util.Collection;
import java.util.HashMap;
import java.util.Objects;
public class MainActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final int MENU_ENTRY_ADD_DEVICE = 1; //0 means no-selection
private static final int MENU_ENTRY_SETTINGS = 2;
private static final int MENU_ENTRY_ABOUT = 3;
private static final int MENU_ENTRY_DEVICE_FIRST_ID = 1000; //All subsequent ids are devices in the menu
private static final int MENU_ENTRY_DEVICE_UNKNOWN = 9999; //It's still a device, but we don't know which one yet
private static final int STORAGE_LOCATION_CONFIGURED = 2020;
private static final String STATE_SELECTED_MENU_ENTRY = "selected_entry"; //Saved only in onSaveInstanceState
private static final String STATE_SELECTED_DEVICE = "selected_device"; //Saved persistently in preferences
public static final int RESULT_NEEDS_RELOAD = Activity.RESULT_FIRST_USER;
public static final String PAIR_REQUEST_STATUS = "pair_req_status";
public static final String PAIRING_ACCEPTED = "accepted";
public static final String PAIRING_REJECTED = "rejected";
public static final String PAIRING_PENDING = "pending";
public static final String EXTRA_DEVICE_ID = "deviceId";
private NavigationView mNavigationView;
private DrawerLayout mDrawerLayout;
private TextView mNavViewDeviceName;
private String mCurrentDevice;
private int mCurrentMenuEntry;
private SharedPreferences preferences;
private final HashMap<MenuItem, String> mMapMenuToDeviceId = new HashMap<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DeviceHelper.initializeDeviceId(this);
final ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
mNavigationView = binding.navigationDrawer;
mDrawerLayout = binding.drawerLayout;
View mDrawerHeader = mNavigationView.getHeaderView(0);
mNavViewDeviceName = mDrawerHeader.findViewById(R.id.device_name);
ImageView mNavViewDeviceType = mDrawerHeader.findViewById(R.id.device_type);
setSupportActionBar(binding.toolbarLayout.toolbar);
ActionBar actionBar = getSupportActionBar();
ActionBarDrawerToggle mDrawerToggle = new ActionBarDrawerToggle(this, /* host Activity */
mDrawerLayout, /* DrawerLayout object */
R.string.open, /* "open drawer" description */
R.string.close /* "close drawer" description */
);
mDrawerLayout.addDrawerListener(mDrawerToggle);
mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
mDrawerToggle.setDrawerIndicatorEnabled(true);
mDrawerToggle.syncState();
preferences = getSharedPreferences("stored_menu_selection", Context.MODE_PRIVATE);
// Note: The preference changed listener should be registered before getting the name, because getting
// it can trigger a background fetch from the internet that will eventually update the preference
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this);
String deviceName = DeviceHelper.getDeviceName(this);
mNavViewDeviceType.setImageDrawable(DeviceHelper.getDeviceType(this).getIcon(this));
mNavViewDeviceName.setText(deviceName);
mNavigationView.setNavigationItemSelectedListener(menuItem -> {
mCurrentMenuEntry = menuItem.getItemId();
switch (mCurrentMenuEntry) {
case MENU_ENTRY_ADD_DEVICE:
mCurrentDevice = null;
preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply();
setContentFragment(new PairingFragment());
break;
case MENU_ENTRY_SETTINGS:
mCurrentDevice = null;
preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply();
setContentFragment(new SettingsFragment());
break;
case MENU_ENTRY_ABOUT:
mCurrentDevice = null;
preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply();
setContentFragment(AboutFragment.newInstance(Objects.requireNonNull(ApplicationAboutDataKt.getApplicationAboutData(this))));
break;
default:
String deviceId = mMapMenuToDeviceId.get(menuItem);
onDeviceSelected(deviceId);
break;
}
mDrawerLayout.closeDrawer(mNavigationView);
return true;
});
// Decide which menu entry should be selected at start
String savedDevice;
int savedMenuEntry;
if (getIntent().hasExtra("forceOverview")) {
Log.i("MainActivity", "Requested to start main overview");
savedDevice = null;
savedMenuEntry = MENU_ENTRY_ADD_DEVICE;
} else if (getIntent().hasExtra(EXTRA_DEVICE_ID)) {
Log.i("MainActivity", "Loading selected device from parameter");
savedDevice = getIntent().getStringExtra(EXTRA_DEVICE_ID);
savedMenuEntry = MENU_ENTRY_DEVICE_UNKNOWN;
// If pairStatus is not empty, then the user has accepted/reject the pairing from the notification
String pairStatus = getIntent().getStringExtra(PAIR_REQUEST_STATUS);
if (pairStatus != null) {
Log.i("MainActivity", "pair status is " + pairStatus);
savedDevice = onPairResultFromNotification(savedDevice, pairStatus);
if (savedDevice == null) {
savedMenuEntry = MENU_ENTRY_ADD_DEVICE;
}
}
} else if (savedInstanceState != null) {
Log.i("MainActivity", "Loading selected device from saved activity state");
savedDevice = savedInstanceState.getString(STATE_SELECTED_DEVICE);
savedMenuEntry = savedInstanceState.getInt(STATE_SELECTED_MENU_ENTRY, MENU_ENTRY_ADD_DEVICE);
} else {
Log.i("MainActivity", "Loading selected device from persistent storage");
savedDevice = preferences.getString(STATE_SELECTED_DEVICE, null);
savedMenuEntry = (savedDevice != null)? MENU_ENTRY_DEVICE_UNKNOWN : MENU_ENTRY_ADD_DEVICE;
}
mCurrentMenuEntry = savedMenuEntry;
mCurrentDevice = savedDevice;
mNavigationView.setCheckedItem(savedMenuEntry);
//FragmentManager will restore whatever fragment was there
if (savedInstanceState != null) {
Fragment frag = getSupportFragmentManager().findFragmentById(R.id.container);
if (!(frag instanceof DeviceFragment) || ((DeviceFragment)frag).getDeviceId().equals(savedDevice)) {
return;
}
}
// Activate the chosen fragment and select the entry in the menu
if (savedMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID && savedDevice != null) {
onDeviceSelected(savedDevice);
} else {
if (mCurrentMenuEntry == MENU_ENTRY_SETTINGS) {
setContentFragment(new SettingsFragment());
} else if (mCurrentMenuEntry == MENU_ENTRY_ABOUT) {
setContentFragment(AboutFragment.newInstance(Objects.requireNonNull(ApplicationAboutDataKt.getApplicationAboutData(this))));
} else {
setContentFragment(new PairingFragment());
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this);
}
private String onPairResultFromNotification(String deviceId, String pairStatus) {
assert(deviceId != null);
if (!pairStatus.equals(PAIRING_PENDING)) {
BackgroundService.RunCommand(this, service -> {
Device device = service.getDevice(deviceId);
if (device == null) {
Log.w("rejectPairing", "Device no longer exists: " + deviceId);
return;
}
if (pairStatus.equals(PAIRING_ACCEPTED)) {
device.acceptPairing();
} else if (pairStatus.equals(PAIRING_REJECTED)) {
device.rejectPairing();
}
});
}
if (pairStatus.equals(PAIRING_ACCEPTED) || pairStatus.equals(PAIRING_PENDING)) {
return deviceId;
} else {
return null;
}
}
private int deviceIdToMenuEntryId(String deviceId) {
for (HashMap.Entry<MenuItem, String> entry : mMapMenuToDeviceId.entrySet()) {
if (TextUtils.equals(entry.getValue(), deviceId)) { //null-safe
return entry.getKey().getItemId();
}
}
return MENU_ENTRY_DEVICE_UNKNOWN;
}
@Override
public void onBackPressed() {
if (mDrawerLayout.isDrawerOpen(mNavigationView)) {
mDrawerLayout.closeDrawer(mNavigationView);
} else if (mCurrentMenuEntry == MENU_ENTRY_SETTINGS || mCurrentMenuEntry == MENU_ENTRY_ABOUT) {
mCurrentMenuEntry = MENU_ENTRY_ADD_DEVICE;
mNavigationView.setCheckedItem(MENU_ENTRY_ADD_DEVICE);
setContentFragment(new PairingFragment());
} else {
super.onBackPressed();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
mDrawerLayout.openDrawer(mNavigationView);
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
private void updateDeviceList() {
BackgroundService.RunCommand(MainActivity.this, service -> {
Menu menu = mNavigationView.getMenu();
menu.clear();
mMapMenuToDeviceId.clear();
SubMenu devicesMenu = menu.addSubMenu(R.string.devices);
int id = MENU_ENTRY_DEVICE_FIRST_ID;
Collection<Device> devices = service.getDevices().values();
for (Device device : devices) {
if (device.isReachable() && device.isPaired()) {
MenuItem item = devicesMenu.add(Menu.FIRST, id++, 1, device.getName());
item.setIcon(device.getIcon());
item.setCheckable(true);
mMapMenuToDeviceId.put(item, device.getDeviceId());
}
}
MenuItem addDeviceItem = devicesMenu.add(Menu.FIRST, MENU_ENTRY_ADD_DEVICE, 1000, R.string.pair_new_device);
addDeviceItem.setIcon(R.drawable.ic_action_content_add_circle_outline_32dp);
addDeviceItem.setCheckable(true);
MenuItem settingsItem = menu.add(Menu.FIRST, MENU_ENTRY_SETTINGS, 1000, R.string.settings);
settingsItem.setIcon(R.drawable.ic_settings_white_32dp);
settingsItem.setCheckable(true);
MenuItem aboutItem = menu.add(Menu.FIRST, MENU_ENTRY_ABOUT, 1000, R.string.about);
aboutItem.setIcon(R.drawable.ic_baseline_info_24);
aboutItem.setCheckable(true);
//Ids might have changed
if (mCurrentMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID) {
mCurrentMenuEntry = deviceIdToMenuEntryId(mCurrentDevice);
}
mNavigationView.setCheckedItem(mCurrentMenuEntry);
});
}
@Override
protected void onStart() {
super.onStart();
BackgroundService.RunCommand(this, service -> {
service.onNetworkChange();
service.addDeviceListChangedCallback("MainActivity", unused -> updateDeviceList());
});
updateDeviceList();
}
@Override
protected void onStop() {
BackgroundService.RunCommand(this, service -> service.removeDeviceListChangedCallback("MainActivity"));
super.onStop();
}
private static void uncheckAllMenuItems(Menu menu) {
int size = menu.size();
for (int i = 0; i < size; i++) {
MenuItem item = menu.getItem(i);
if(item.hasSubMenu()) {
uncheckAllMenuItems(item.getSubMenu());
} else {
item.setChecked(false);
}
}
}
public void onDeviceSelected(String deviceId, boolean fromDeviceList) {
mCurrentDevice = deviceId;
preferences.edit().putString(STATE_SELECTED_DEVICE, deviceId).apply();
if (mCurrentDevice != null) {
mCurrentMenuEntry = deviceIdToMenuEntryId(deviceId);
if (mCurrentMenuEntry == MENU_ENTRY_DEVICE_UNKNOWN) {
uncheckAllMenuItems(mNavigationView.getMenu());
} else {
mNavigationView.setCheckedItem(mCurrentMenuEntry);
}
setContentFragment(DeviceFragment.Companion.newInstance(deviceId, fromDeviceList));
} else {
mCurrentMenuEntry = MENU_ENTRY_ADD_DEVICE;
mNavigationView.setCheckedItem(mCurrentMenuEntry);
setContentFragment(new PairingFragment());
}
}
private void setContentFragment(Fragment fragment) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.container, fragment)
.commit();
}
public void onDeviceSelected(String deviceId) {
onDeviceSelected(deviceId, false);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(STATE_SELECTED_DEVICE, mCurrentDevice);
outState.putInt(STATE_SELECTED_MENU_ENTRY, mCurrentMenuEntry);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == RESULT_NEEDS_RELOAD) {
BackgroundService.RunCommand(this, service -> {
Device device = service.getDevice(mCurrentDevice);
device.reloadPluginsFromSettings();
});
} else if (requestCode == STORAGE_LOCATION_CONFIGURED && resultCode == RESULT_OK && data != null){
Uri uri = data.getData();
ShareSettingsFragment.saveStorageLocationPreference(this, uri);
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode,permissions,grantResults);
boolean permissionsGranted = ArrayUtils.contains(grantResults, PackageManager.PERMISSION_GRANTED);
if (permissionsGranted) {
int i = ArrayUtils.indexOf(permissions, Manifest.permission.WRITE_EXTERNAL_STORAGE);
boolean writeStoragePermissionGranted = (i != ArrayUtils.INDEX_NOT_FOUND &&
grantResults[i] == PackageManager.PERMISSION_GRANTED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && writeStoragePermissionGranted) {
// 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.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, STORAGE_LOCATION_CONFIGURED);
}
//New permission granted, reload plugins
BackgroundService.RunCommand(this, service -> {
Device device = service.getDevice(mCurrentDevice);
device.reloadPluginsFromSettings();
});
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (DeviceHelper.KEY_DEVICE_NAME_PREFERENCE.equals(key)) {
mNavViewDeviceName.setText(DeviceHelper.getDeviceName(this));
BackgroundService.RunCommand(this, BackgroundService::onNetworkChange); //Re-send our identity packet
}
}
}

View File

@ -0,0 +1,376 @@
package org.kde.kdeconnect.UserInterface
import android.Manifest
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import com.google.android.material.navigation.NavigationView
import org.apache.commons.lang3.ArrayUtils
import org.kde.kdeconnect.BackgroundService
import org.kde.kdeconnect.Device
import org.kde.kdeconnect.Helpers.DeviceHelper
import org.kde.kdeconnect.Plugins.SharePlugin.ShareSettingsFragment
import org.kde.kdeconnect.UserInterface.About.AboutFragment.Companion.newInstance
import org.kde.kdeconnect.UserInterface.About.getApplicationAboutData
import org.kde.kdeconnect.UserInterface.DeviceFragment.Companion.newInstance
import org.kde.kdeconnect_tp.R
import org.kde.kdeconnect_tp.databinding.ActivityMainBinding
import java.util.*
private const val MENU_ENTRY_ADD_DEVICE = 1 //0 means no-selection
private const val MENU_ENTRY_SETTINGS = 2
private const val MENU_ENTRY_ABOUT = 3
private const val MENU_ENTRY_DEVICE_FIRST_ID = 1000 //All subsequent ids are devices in the menu
private const val MENU_ENTRY_DEVICE_UNKNOWN = 9999 //It's still a device, but we don't know which one yet
private const val STORAGE_LOCATION_CONFIGURED = 2020
private const val STATE_SELECTED_MENU_ENTRY = "selected_entry" //Saved only in onSaveInstanceState
private const val STATE_SELECTED_DEVICE = "selected_device" //Saved persistently in preferences
class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val mNavigationView: NavigationView by lazy { binding.navigationDrawer }
private val mDrawerLayout: DrawerLayout? by lazy { binding.drawerLayout }
private lateinit var mNavViewDeviceName: TextView
private var mCurrentDevice: String? = null
private var mCurrentMenuEntry = 0
private val preferences: SharedPreferences by lazy { getSharedPreferences("stored_menu_selection", MODE_PRIVATE) }
private val mMapMenuToDeviceId = HashMap<MenuItem, String>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DeviceHelper.initializeDeviceId(this)
setContentView(binding.root)
val mDrawerHeader = mNavigationView.getHeaderView(0)
mNavViewDeviceName = mDrawerHeader.findViewById(R.id.device_name)
val mNavViewDeviceType = mDrawerHeader.findViewById<ImageView>(R.id.device_type)
setSupportActionBar(binding.toolbarLayout.toolbar)
mDrawerLayout?.let {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val mDrawerToggle = ActionBarDrawerToggle(
this, /* host Activity */
it, /* DrawerLayout object */
R.string.open, /* "open drawer" description */
R.string.close /* "close drawer" description */
).apply {
isDrawerIndicatorEnabled = true
syncState()
}
it.addDrawerListener(mDrawerToggle)
it.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START)
} ?: {
supportActionBar?.setDisplayShowHomeEnabled(false)
supportActionBar?.setHomeButtonEnabled(false)
}
// Note: The preference changed listener should be registered before getting the name, because getting
// it can trigger a background fetch from the internet that will eventually update the preference
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this)
val deviceName = DeviceHelper.getDeviceName(this)
mNavViewDeviceType?.setImageDrawable(DeviceHelper.getDeviceType(this).getIcon(this))
mNavViewDeviceName.text = deviceName
mNavigationView.setNavigationItemSelectedListener { menuItem: MenuItem ->
mCurrentMenuEntry = menuItem.itemId
when (mCurrentMenuEntry) {
MENU_ENTRY_ADD_DEVICE -> {
mCurrentDevice = null
preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply()
setContentFragment(PairingFragment())
}
MENU_ENTRY_SETTINGS -> {
mCurrentDevice = null
preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply()
setContentFragment(SettingsFragment())
}
MENU_ENTRY_ABOUT -> {
mCurrentDevice = null
preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply()
setContentFragment(newInstance(getApplicationAboutData(this)))
}
else -> {
val deviceId = mMapMenuToDeviceId[menuItem]
onDeviceSelected(deviceId)
}
}
mDrawerLayout?.closeDrawer(mNavigationView)
true
}
// Decide which menu entry should be selected at start
var savedDevice: String?
var savedMenuEntry: Int
when {
intent.hasExtra(FLAG_FORCE_OVERVIEW) -> {
Log.i(this::class.simpleName, "Requested to start main overview")
savedDevice = null
savedMenuEntry = MENU_ENTRY_ADD_DEVICE
}
intent.hasExtra(EXTRA_DEVICE_ID) -> {
Log.i(this::class.simpleName, "Loading selected device from parameter")
savedDevice = intent.getStringExtra(EXTRA_DEVICE_ID)
savedMenuEntry = MENU_ENTRY_DEVICE_UNKNOWN
// If pairStatus is not empty, then the user has accepted/reject the pairing from the notification
val pairStatus = intent.getStringExtra(PAIR_REQUEST_STATUS)
if (pairStatus != null) {
Log.i(this::class.simpleName, "Pair status is $pairStatus")
savedDevice = onPairResultFromNotification(savedDevice, pairStatus)
if (savedDevice == null) {
savedMenuEntry = MENU_ENTRY_ADD_DEVICE
}
}
}
savedInstanceState != null -> {
Log.i(this::class.simpleName, "Loading selected device from saved activity state")
savedDevice = savedInstanceState.getString(STATE_SELECTED_DEVICE)
savedMenuEntry = savedInstanceState.getInt(STATE_SELECTED_MENU_ENTRY, MENU_ENTRY_ADD_DEVICE)
}
else -> {
Log.i(this::class.simpleName, "Loading selected device from persistent storage")
savedDevice = preferences.getString(STATE_SELECTED_DEVICE, null)
savedMenuEntry = if (savedDevice != null) MENU_ENTRY_DEVICE_UNKNOWN else MENU_ENTRY_ADD_DEVICE
}
}
mCurrentMenuEntry = savedMenuEntry
mCurrentDevice = savedDevice
mNavigationView.setCheckedItem(savedMenuEntry)
//FragmentManager will restore whatever fragment was there
if (savedInstanceState != null) {
val frag = supportFragmentManager.findFragmentById(R.id.container)
if (frag !is DeviceFragment || frag.deviceId == savedDevice) return
}
// Activate the chosen fragment and select the entry in the menu
if (savedMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID && savedDevice != null) {
onDeviceSelected(savedDevice)
} else {
when (mCurrentMenuEntry) {
MENU_ENTRY_SETTINGS -> setContentFragment(SettingsFragment())
MENU_ENTRY_ABOUT -> setContentFragment(newInstance(Objects.requireNonNull(getApplicationAboutData(this))))
else -> setContentFragment(PairingFragment())
}
}
}
override fun onDestroy() {
super.onDestroy()
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this)
}
private fun onPairResultFromNotification(deviceId: String?, pairStatus: String): String? {
assert(deviceId != null)
if (pairStatus != PAIRING_PENDING) {
BackgroundService.RunCommand(this) { service: BackgroundService ->
val device = service.getDevice(deviceId)
if (device == null) {
Log.w(this::class.simpleName, "Reject pairing - device no longer exists: $deviceId")
return@RunCommand
}
when (pairStatus) {
PAIRING_ACCEPTED -> device.acceptPairing()
PAIRING_REJECTED -> device.rejectPairing()
}
}
}
return if (pairStatus == PAIRING_ACCEPTED || pairStatus == PAIRING_PENDING) deviceId else null
}
private fun deviceIdToMenuEntryId(deviceId: String?): Int {
for ((key, value) in mMapMenuToDeviceId) {
if (value == deviceId) {
return key.itemId
}
}
return MENU_ENTRY_DEVICE_UNKNOWN
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
mDrawerLayout?.let {
it.closeDrawer(mNavigationView)
return
}
if (mCurrentMenuEntry == MENU_ENTRY_SETTINGS || mCurrentMenuEntry == MENU_ENTRY_ABOUT) {
mCurrentMenuEntry = MENU_ENTRY_ADD_DEVICE
mNavigationView.setCheckedItem(MENU_ENTRY_ADD_DEVICE)
setContentFragment(PairingFragment())
} else {
super.onBackPressed()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean =
if (item.itemId == android.R.id.home) {
mDrawerLayout?.openDrawer(mNavigationView)
true
} else {
super.onOptionsItemSelected(item)
}
private fun updateDeviceList() {
BackgroundService.RunCommand(this@MainActivity) { service: BackgroundService ->
val menu = mNavigationView.menu
menu.clear()
mMapMenuToDeviceId.clear()
val devicesMenu = menu.addSubMenu(R.string.devices)
var id = MENU_ENTRY_DEVICE_FIRST_ID
val devices: Collection<Device> = service.devices.values
for (device in devices) {
if (device.isReachable && device.isPaired) {
val item = devicesMenu.add(Menu.FIRST, id++, 1, device.name)
item.setIcon(device.icon)
item.setCheckable(true)
mMapMenuToDeviceId[item] = device.deviceId
}
}
val addDeviceItem = devicesMenu.add(Menu.FIRST, MENU_ENTRY_ADD_DEVICE, 1000, R.string.pair_new_device)
addDeviceItem.setIcon(R.drawable.ic_action_content_add_circle_outline_32dp)
addDeviceItem.setCheckable(true)
val settingsItem = menu.add(Menu.FIRST, MENU_ENTRY_SETTINGS, 1000, R.string.settings)
settingsItem.setIcon(R.drawable.ic_settings_white_32dp)
settingsItem.setCheckable(true)
val aboutItem = menu.add(Menu.FIRST, MENU_ENTRY_ABOUT, 1000, R.string.about)
aboutItem.setIcon(R.drawable.ic_baseline_info_24)
aboutItem.setCheckable(true)
//Ids might have changed
if (mCurrentMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID) {
mCurrentMenuEntry = deviceIdToMenuEntryId(mCurrentDevice)
}
mNavigationView.setCheckedItem(mCurrentMenuEntry)
}
}
override fun onStart() {
super.onStart()
BackgroundService.RunCommand(this) { service: BackgroundService ->
service.onNetworkChange()
service.addDeviceListChangedCallback(this::class.simpleName) { updateDeviceList() }
}
updateDeviceList()
}
override fun onStop() {
BackgroundService.RunCommand(this) { service: BackgroundService -> service.removeDeviceListChangedCallback(this::class.simpleName) }
super.onStop()
}
@JvmOverloads
fun onDeviceSelected(deviceId: String?, fromDeviceList: Boolean = false) {
mCurrentDevice = deviceId
preferences.edit().putString(STATE_SELECTED_DEVICE, deviceId).apply()
if (mCurrentDevice != null) {
mCurrentMenuEntry = deviceIdToMenuEntryId(deviceId)
if (mCurrentMenuEntry == MENU_ENTRY_DEVICE_UNKNOWN) {
uncheckAllMenuItems(mNavigationView.menu)
} else {
mNavigationView.setCheckedItem(mCurrentMenuEntry)
}
setContentFragment(newInstance(deviceId, fromDeviceList))
} else {
mCurrentMenuEntry = MENU_ENTRY_ADD_DEVICE
mNavigationView.setCheckedItem(mCurrentMenuEntry)
setContentFragment(PairingFragment())
}
}
private fun setContentFragment(fragment: Fragment) {
supportFragmentManager
.beginTransaction()
.replace(R.id.container, fragment)
.commit()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(STATE_SELECTED_DEVICE, mCurrentDevice)
outState.putInt(STATE_SELECTED_MENU_ENTRY, mCurrentMenuEntry)
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when {
requestCode == RESULT_NEEDS_RELOAD -> BackgroundService.RunCommand(this) { service: BackgroundService ->
val device = service.getDevice(mCurrentDevice)
device.reloadPluginsFromSettings()
}
requestCode == STORAGE_LOCATION_CONFIGURED && resultCode == RESULT_OK && data != null -> {
val uri = data.data
ShareSettingsFragment.saveStorageLocationPreference(this, uri)
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
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) {
// 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)
}
//New permission granted, reload plugins
BackgroundService.RunCommand(this) { service: BackgroundService ->
val device = service.getDevice(mCurrentDevice)
device.reloadPluginsFromSettings()
}
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (DeviceHelper.KEY_DEVICE_NAME_PREFERENCE == key) {
mNavViewDeviceName.text = DeviceHelper.getDeviceName(this)
BackgroundService.RunCommand(this) { obj: BackgroundService -> obj.onNetworkChange() } //Re-send our identity packet
}
}
private fun uncheckAllMenuItems(menu: Menu) {
val size = menu.size()
for (i in 0 until size) {
val item = menu.getItem(i)
item.subMenu?.let { uncheckAllMenuItems(it) } ?: item.setChecked(false)
}
}
companion object {
const val EXTRA_DEVICE_ID = "deviceId"
const val PAIR_REQUEST_STATUS = "pair_req_status"
const val PAIRING_ACCEPTED = "accepted"
const val PAIRING_REJECTED = "rejected"
const val PAIRING_PENDING = "pending"
const val RESULT_NEEDS_RELOAD = RESULT_FIRST_USER
const val FLAG_FORCE_OVERVIEW = "forceOverview"
}
}