如何自动安装 WearOS 配套应用程序以及 Android 手机应用程序

问题描述 投票:0回答:2

我使用 Google Play 控制台将我的 Android 和 WearOS 应用作为单独的 APK 发布多 APK 交付方法

从各自的设备浏览时都可以发现这两个应用程序 - 手机上的 Android 应用程序和手表上的 WearOS 应用程序。此外,当手机应用程序安装在 Android 设备上时,我可以在我的 WearOS 设备上看到我可以在 Play 商店应用程序的“手机上的应用程序”屏幕上安装配套应用程序。

Google 的官方 WearOS 文档 声明如下:

在运行 Wear 2.0 的设备上,当用户安装具有 关联的手表应用(嵌入式 APK 或通过 Play 管理中心),用户会收到有关 可用的手表应用程序。点击通知打开手表播放 商店,为用户提供安装手表应用程序的选项。

但是,当手机上安装了Android应用程序时,没有任何反应。此外,用户不知道该应用程序有 WearOS 配套应用程序,因为它在手机 Play 商店应用程序或网站上不可见。手表也是如此——当用户从他们的 WearOS 设备上发现我的应用程序并安装它时,手机对应的应用程序没有安装,也没有通知用户。

WearOS 应用程序不是独立的,因此需要手机应用程序才能运行。它具有相同的包名称并使用相同的密钥签名。手表和手机上的 WearOS 应用程序允许所有通知。

那么,有没有办法自动安装 WearOS 应用程序,或者至少让用户知道他们可以安装它?谢谢!

android google-play wear-os google-play-console
2个回答
8
投票

自 Wear OS 2.0 以来,无法完全自动化。谷歌全力投入“独立”Wear 应用程序的想法,而让我们这些集成应用程序的开发人员大多处于冷落状态。

据我所知,自 Wear 2.0 以来,安装这两个应用程序的唯一方法是如下流程:

  1. 当用户运行您的手机应用程序时,使用 Capability API 检查您的应用程序是否安装在配对的手表上。
  2. 如果没有安装,向用户展示一个合适的用户界面,告诉他们这个问题。
  3. 然后从该 UI 中,使用
    RemoteIntent.startRemoteActivity()
    给他们一个操作,将手表上的 Play 商店打开到您的应用列表。

你需要在你的手表应用程序中做类似的事情,以防用户先安装并运行它。

这里记录了这个过程(带有一些代码示例):https://developer.android.com/training/wearables/apps/standalone-apps#detecting-your-app


0
投票

您可以使用这些 GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener 方法,但它们已被弃用

这里是例子

class WearOsActivity : AppCompatActivity(), CapabilityClient.OnCapabilityChangedListener,
GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {


private lateinit var signInAccount: GoogleSignInAccount
private lateinit var mGoogleApiClient: GoogleApiClient
private lateinit var capabilityClient: CapabilityClient
private lateinit var nodeClient: NodeClient
private lateinit var remoteActivityHelper: RemoteActivityHelper

private var wearNodesWithApp: Set<Node>? = null
private var allConnectedNodes: List<Node>? = null

val remoteOpenButton by lazy { findViewById<Button>(R.id.connect_watch) }
val informationTextView by lazy { findViewById<TextView>(R.id.informationTextView) }
private val fitnessOptions =
    FitnessOptions.builder().addDataType(DataType.TYPE_STEP_COUNT_DELTA).build()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_wear_os)

    capabilityClient = Wearable.getCapabilityClient(this)
    nodeClient = Wearable.getNodeClient(this)
    remoteActivityHelper = RemoteActivityHelper(this)

    mGoogleApiClient = GoogleApiClient.Builder(this)
        .addApi(Fitness.HISTORY_API)
        .addApi(Fitness.RECORDING_API)
        .addScope(Scope(Scopes.PROFILE))
        .build()
    mGoogleApiClient.connect()


    if (!GoogleSignIn.hasPermissions(
            GoogleSignIn.getLastSignedInAccount(this),
            fitnessOptions
        )
    ) {
        signInAccount = GoogleSignIn.getAccountForExtension(this, fitnessOptions)
        GoogleSignIn.requestPermissions(
            this, // or FragmentActivity
            0,
            signInAccount,
            fitnessOptions
        )
    } else {
        // If permissions are already granted, directly call the function to get total calories
        getTotalCalories()

    }


    val fitnessOptions = FitnessOptions.builder()
        .addDataType(DataType.TYPE_CALORIES_EXPENDED, FitnessOptions.ACCESS_READ)
        .build()

    if (!GoogleSignIn.hasPermissions(
            GoogleSignIn.getLastSignedInAccount(this),
            fitnessOptions
        )
    ) {
        val signInAccount = GoogleSignIn.getAccountForExtension(this, fitnessOptions)
        GoogleSignIn.requestPermissions(
            this, // or FragmentActivity
            0,
            signInAccount,
            fitnessOptions
        )
    } else {
        // If permissions are already granted, directly call the function to get total calories
        getTotalCalories()
    }


    remoteOpenButton.setOnClickListener {
        openPlayStoreOnWearDevicesWithoutApp()
    }
    updateUI()

    lifecycleScope.launch {
        lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
            launch {
                // Initial request for devices with our capability, aka, our Wear app installed.
                findWearDevicesWithApp()
            }
            launch {
                // Initial request for all Wear devices connected (with or without our capability).
                // Additional Note: Because there isn't a listener for ALL Nodes added/removed from network
                // that isn't deprecated, we simply update the full list when the Google API Client is
                // connected and when capability changes come through in the onCapabilityChanged() method.
                findAllWearDevices()
            }
        }
    }
}

override fun onCapabilityChanged(capabilityInfo: CapabilityInfo) {
    Log.d(TAG, "onCapabilityChanged(): $capabilityInfo")
    wearNodesWithApp = capabilityInfo.nodes

    lifecycleScope.launch {
        // Because we have an updated list of devices with/without our app, we need to also update
        // our list of active Wear devices.
        findAllWearDevices()
    }
}


override fun onPause() {
    Log.d(TAG, "onPause()")
    super.onPause()
    capabilityClient.removeListener(this, CAPABILITY_WEAR_APP)
}

override fun onResume() {
    Log.d(TAG, "onResume()")
    super.onResume()
    capabilityClient.addListener(this, CAPABILITY_WEAR_APP)
}

private fun getTotalCalories() {
    val endTime = LocalDateTime.now().atZone(ZoneId.systemDefault())
    val startTime = endTime.minusDays(1)
    Log.i(TAG, "Range Start: $startTime")
    Log.i(TAG, "Range End: $endTime")

    val readStepsRequest =
        DataReadRequest.Builder()
            // The data request can specify multiple data types to return,
            // effectively combining multiple data queries into one call.
            // This example demonstrates aggregating only one data type.
            .aggregate(DataType.AGGREGATE_STEP_COUNT_DELTA)
            // Analogous to a "Group By" in SQL, defines how data should be
            // aggregated.
            // bucketByTime allows for a time span, whereas bucketBySession allows
            // bucketing by <a href="/fit/android/using-sessions">sessions</a>.
            .bucketByTime(1, TimeUnit.DAYS)
            .setTimeRange(startTime.toEpochSecond(), endTime.toEpochSecond(), TimeUnit.SECONDS)
            .build()

    val readCalRequest = DataReadRequest.Builder()
        .aggregate(DataType.AGGREGATE_CALORIES_EXPENDED)
        .bucketByActivityType(1, TimeUnit.SECONDS)
        .setTimeRange(startTime.toEpochSecond(), endTime.toEpochSecond(), TimeUnit.SECONDS)
        .build()

    Fitness.getHistoryClient(this, GoogleSignIn.getAccountForExtension(this, fitnessOptions))
        .readData(readCalRequest)
        .addOnSuccessListener { response ->
            // The aggregate query puts datasets into buckets, so flatten into a
            // single list of datasets
            for (dataSet in response.buckets.flatMap { it.dataSets }) {
                dumpDataSet(dataSet)
            }
        }
        .addOnFailureListener { e ->
            Log.w(TAG, "There was an error reading data from Google Fit", e)
        }

}

private fun dumpDataSet(dataSet: DataSet) {
    Log.i(TAG, "Data returned for Data type: ${dataSet.dataType.name}")
    for (dp in dataSet.dataPoints) {
        Log.i(TAG, "Data point:")
        Log.i(TAG, "\tType: ${dp.dataType.name}")
        Log.i(TAG, "\tStart: ${dp.getStartTimeString()}")
        Log.i(TAG, "\tEnd: ${dp.getEndTimeString()}")
        for (field in dp.dataType.fields) {
            Log.i(TAG, "\tField: ${field.name.toString()} Value: ${dp.getValue(field)}")
        }
    }
}

private fun DataPoint.getStartTimeString() =
    Instant.ofEpochSecond(this.getStartTime(TimeUnit.SECONDS))
        .atZone(ZoneId.systemDefault())
        .toLocalDateTime().toString()

private fun DataPoint.getEndTimeString() =
    Instant.ofEpochSecond(this.getEndTime(TimeUnit.SECONDS))
        .atZone(ZoneId.systemDefault())
        .toLocalDateTime().toString()


private suspend fun findWearDevicesWithApp() {
    Log.d(TAG, "findWearDevicesWithApp()")

    try {
        val capabilityInfo = capabilityClient
            .getCapability(CAPABILITY_WEAR_APP, CapabilityClient.FILTER_ALL)
            .await()

        withContext(Dispatchers.Main) {
            Log.d(TAG, "Capability request succeeded.")
            wearNodesWithApp = capabilityInfo.nodes
            Log.d(TAG, "Capable Nodes: $wearNodesWithApp")
            updateUI()
        }
    } catch (cancellationException: CancellationException) {
        // Request was cancelled normally
        throw cancellationException
    } catch (throwable: Throwable) {
        Log.d(TAG, "Capability request failed to return any results.")
    }
}

private suspend fun findAllWearDevices() {
    Log.d(TAG, "findAllWearDevices()")

    try {
        val connectedNodes = nodeClient.connectedNodes.await()

        withContext(Dispatchers.Main) {
            allConnectedNodes = connectedNodes
            updateUI()
        }
    } catch (cancellationException: CancellationException) {
        // Request was cancelled normally
    } catch (throwable: Throwable) {
        Log.d(TAG, "Node request failed to return any results.")
    }
}

private fun updateUI() {
    Log.d(TAG, "updateUI()")

    val wearNodesWithApp = wearNodesWithApp
    val allConnectedNodes = allConnectedNodes

    when {
        wearNodesWithApp == null || allConnectedNodes == null -> {
            Log.d(TAG, "Waiting on Results for both connected nodes and nodes with app")
            informationTextView.text = getString(R.string.message_checking)
            remoteOpenButton.alpha = 0.5f
            remoteOpenButton.isEnabled = false
        }
        allConnectedNodes.isEmpty() -> {
            Log.d(TAG, "No devices")
            informationTextView.text = getString(R.string.message_checking)
            remoteOpenButton.alpha = 0.5f
            remoteOpenButton.isEnabled = false
        }
        wearNodesWithApp.isEmpty() -> {
            Log.d(TAG, "Missing on all devices")
            informationTextView.text = getString(R.string.message_missing_all)
            remoteOpenButton.alpha = 1f
            remoteOpenButton.isEnabled = true
        }
        wearNodesWithApp.size < allConnectedNodes.size -> {
            // TODO: Add your code to communicate with the wear app(s) via Wear APIs
            //       (MessageClient, DataClient, etc.)
            Log.d(TAG, "Installed on some devices")
            informationTextView.text =
                getString(R.string.message_some_installed, wearNodesWithApp.toString())
            remoteOpenButton.alpha = 1f
            remoteOpenButton.isEnabled = true
        }
        else -> {
            // TODO: Add your code to communicate with the wear app(s) via Wear APIs
            //       (MessageClient, DataClient, etc.)
            Log.d(TAG, "Installed on all devices")
            informationTextView.text =
                getString(R.string.message_all_installed, wearNodesWithApp.toString())
            remoteOpenButton.alpha = 0.5f
            remoteOpenButton.isEnabled = false
        }
    }
}

private fun openPlayStoreOnWearDevicesWithoutApp() {
    Log.d(TAG, "openPlayStoreOnWearDevicesWithoutApp()")

    val wearNodesWithApp = wearNodesWithApp ?: return
    val allConnectedNodes = allConnectedNodes ?: return

    // Determine the list of nodes (wear devices) that don't have the app installed yet.
    val nodesWithoutApp = allConnectedNodes - wearNodesWithApp

    Log.d(TAG, "Number of nodes without app: " + nodesWithoutApp.size)
    val intent = Intent(Intent.ACTION_VIEW)
        .addCategory(Intent.CATEGORY_BROWSABLE)
        .setData(Uri.parse(PLAY_STORE_APP_URI))

    // In parallel, start remote activity requests for all wear devices that don't have the app installed yet.
    nodesWithoutApp.forEach { node ->
        lifecycleScope.launch {
            try {
                remoteActivityHelper
                    .startRemoteActivity(
                        targetIntent = intent,
                        targetNodeId = node.id
                    )
                    .await()

                Toast.makeText(
                    this@WearOsActivity,
                    "The App is Successfully Installed on your wearOs",
                    Toast.LENGTH_SHORT
                ).show()
            } catch (cancellationException: CancellationException) {
                // Request was cancelled normally
            } catch (throwable: Throwable) {
                Toast.makeText(
                    this@WearOsActivity,
                    "Install request failed",
                    Toast.LENGTH_LONG
                ).show()
            }
        }
    }
}


companion object {
    private const val TAG = "MainMobileActivity"

    // Name of capability listed in Wear app's wear.xml.
    // IMPORTANT NOTE: This should be named differently than your Phone app's capability.
    private const val CAPABILITY_WEAR_APP = "verify_remote_example_wear_app"

    // Links to Wear app (Play Store).
    // TODO: Replace with your links/packages.
    private const val PLAY_STORE_APP_URI =
        "market://details?id=com.yewapp"
}

override fun onConnected(p0: Bundle?) {
    TODO("Not yet implemented")
}

override fun onConnectionSuspended(p0: Int) {
    TODO("Not yet implemented")
}

override fun onConnectionFailed(p0: ConnectionResult) {
    TODO("Not yet implemented")
}

}

© www.soinside.com 2019 - 2024. All rights reserved.