1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.media.controls.shared.model
18 
19 import android.app.smartspace.SmartspaceAction
20 import android.content.Context
21 import android.content.Intent
22 import android.content.pm.PackageManager
23 import android.os.Process
24 import android.text.TextUtils
25 import android.util.Log
26 import androidx.annotation.VisibleForTesting
27 import com.android.internal.logging.InstanceId
28 
29 @VisibleForTesting const val KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME"
30 
31 /** State of a Smartspace media recommendations view. */
32 data class SmartspaceMediaData(
33     /** Unique id of a Smartspace media target. */
34     val targetId: String = "INVALID",
35     /** Indicates if the status is active. */
36     val isActive: Boolean = false,
37     /** Package name of the media recommendations' provider-app. */
38     val packageName: String = "INVALID",
39     /** Action to perform when the card is tapped. Also contains the target's extra info. */
40     val cardAction: SmartspaceAction? = null,
41     /** List of media recommendations. */
42     val recommendations: List<SmartspaceAction> = emptyList(),
43     /** Intent for the user's initiated dismissal. */
44     val dismissIntent: Intent? = null,
45     /** The timestamp in milliseconds that the card was generated */
46     val headphoneConnectionTimeMillis: Long = 0L,
47     /** Instance ID for [MediaUiEventLogger] */
48     val instanceId: InstanceId? = null,
49     /** The timestamp in milliseconds indicating when the card should be removed */
50     val expiryTimeMs: Long = 0L,
51 ) {
52     /**
53      * Indicates if all the data is valid.
54      *
55      * TODO(b/230333302): Make MediaControlPanel more flexible so that we can display fewer than
56      *
57      * ```
58      *     [NUM_REQUIRED_RECOMMENDATIONS].
59      * ```
60      */
isValidnull61     fun isValid() = getValidRecommendations().size >= NUM_REQUIRED_RECOMMENDATIONS
62 
63     /** Returns the list of [recommendations] that have valid data. */
64     fun getValidRecommendations() = recommendations.filter { it.icon != null }
65 
66     /** Returns the upstream app name if available. */
getAppNamenull67     fun getAppName(context: Context): CharSequence? {
68         val nameFromAction = cardAction?.intent?.extras?.getString(KEY_SMARTSPACE_APP_NAME)
69         if (!TextUtils.isEmpty(nameFromAction)) {
70             return nameFromAction
71         }
72 
73         val packageManager = context.packageManager
74         packageManager.getLaunchIntentForPackage(packageName)?.let {
75             val launchActivity = it.resolveActivityInfo(packageManager, 0)
76             return launchActivity.loadLabel(packageManager)
77         }
78 
79         Log.w(
80             TAG,
81             "Package $packageName does not have a main launcher activity. " +
82                 "Fallback to full app name"
83         )
84         return try {
85             val applicationInfo = packageManager.getApplicationInfo(packageName, /* flags= */ 0)
86             packageManager.getApplicationLabel(applicationInfo)
87         } catch (e: PackageManager.NameNotFoundException) {
88             null
89         }
90     }
91 
getUidnull92     fun getUid(context: Context): Int {
93         return try {
94             context.packageManager.getApplicationInfo(packageName, 0 /* flags */).uid
95         } catch (e: PackageManager.NameNotFoundException) {
96             Log.w(TAG, "Fail to get media recommendation's app info", e)
97             Process.INVALID_UID
98         }
99     }
100 }
101 
102 /** Key to indicate whether this card should be used to re-show recent media */
103 const val EXTRA_KEY_TRIGGER_RESUME = "SHOULD_TRIGGER_RESUME"
104 /** Key for extras [SmartspaceMediaData.cardAction] indicating why the card was sent */
105 const val EXTRA_KEY_TRIGGER_SOURCE = "MEDIA_RECOMMENDATION_TRIGGER_SOURCE"
106 /** Value for [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent on headphone connection */
107 const val EXTRA_VALUE_TRIGGER_HEADPHONE = "HEADPHONE_CONNECTION"
108 /** Value for key [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent as a regular update */
109 const val EXTRA_VALUE_TRIGGER_PERIODIC = "PERIODIC_TRIGGER"
110 
111 const val NUM_REQUIRED_RECOMMENDATIONS = 3
112 private val TAG = SmartspaceMediaData::class.simpleName!!
113