1 /*
2  * Copyright (C) 2024 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.car.carlauncher.repositories.appactions
18 
19 import android.app.ActivityOptions
20 import android.car.Car
21 import android.car.media.CarMediaManager
22 import android.content.ComponentName
23 import android.content.Context
24 import android.content.Intent
25 import android.content.pm.PackageManager
26 import android.util.Log
27 import com.android.car.carlauncher.R
28 import java.net.URISyntaxException
29 
30 /**
31  * A sealed class representing various ways to launch applications within the system.
32  * Subclasses define specialized launch behaviors for different app types (launcher
33  * activities, media services, disabled apps, etc.).
34  *
35  * @see LauncherActivityLaunchProvider
36  * @see MediaServiceLaunchProvider
37  * @see DisabledAppLaunchProvider
38  * @see TosDisabledAppLaunchProvider
39  * @see MirroringAppLaunchProvider
40  */
41 sealed class AppLaunchProvider {
42 
43     /**
44      *  Launches the app associated with the given ComponentName. This is the core
45      *  method that all subclasses must implement.
46      *
47      *  @param context Application context used for launching the activity.
48      *  @param componentName The ComponentName identifying the app to launch.
49      *  @param launchIntent An optional Intent that might contain additional launch instructions.
50      */
launchnull51     abstract fun launch(
52         context: Context,
53         componentName: ComponentName,
54         launchIntent: Intent? = null
55     )
56 
57     /**
58      * Utility method to launch an Intent with consistent ActivityOptions
59      */
60     protected fun launchApp(context: Context, intent: Intent) {
61         val options = ActivityOptions.makeBasic()
62         options.launchDisplayId = context.display?.displayId ?: 0
63         context.startActivity(intent, options.toBundle())
64     }
65 
66     /**
67      * Handles launching standard launcher activities. It constructs an Intent with
68      * the necessary flags for launching a new instance of the given app.
69      */
70     internal object LauncherActivityLaunchProvider : AppLaunchProvider() {
launchnull71         override fun launch(context: Context, componentName: ComponentName, launchIntent: Intent?) {
72             launchApp(
73                 context,
74                 Intent(Intent.ACTION_MAIN).setComponent(componentName)
75                     .addCategory(Intent.CATEGORY_LAUNCHER).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
76             )
77         }
78     }
79 
80     /**
81      * Launches media services, either by directly starting a media center template or
82      * switching active media sources. This provider works in conjunction with the CarMediaManager.
83      *
84      * @param carMediaManager Interface for controlling car media settings.
85      * @param launchMediaCenter If true, launches the media center template directly.
86      * @param closeScreen A callback function, likely used to close a prior UI screen.
87      */
88     internal class MediaServiceLaunchProvider(
89         private val carMediaManager: CarMediaManager,
90         private val launchMediaCenter: Boolean,
91         val closeScreen: () -> Unit
92     ) : AppLaunchProvider() {
launchnull93         override fun launch(context: Context, componentName: ComponentName, launchIntent: Intent?) {
94             if (launchMediaCenter) {
95                 launchApp(
96                     context,
97                     Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE).putExtra(
98                         Car.CAR_EXTRA_MEDIA_COMPONENT,
99                         componentName.flattenToString()
100                     )
101                 )
102                 return
103             }
104             carMediaManager.setMediaSource(componentName, CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)
105             closeScreen()
106         }
107     }
108 
109     /**
110      * Responsible for enabling and then launching disabled applications. Requires access
111      * to the PackageManager to manage application state.
112      */
113     internal class DisabledAppLaunchProvider(
114         private val packageManager: PackageManager,
115     ) : AppLaunchProvider() {
launchnull116         override fun launch(context: Context, componentName: ComponentName, launchIntent: Intent?) {
117             packageManager.setApplicationEnabledSetting(
118                 componentName.packageName,
119                 PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
120                 0
121             )
122             // Fetch the current enabled setting to make sure the setting is synced
123             // before launching the activity. Otherwise, the activity may not
124             // launch.
125             check(
126                 packageManager.getApplicationEnabledSetting(componentName.packageName)
127                         == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
128             ) {
129                 ("Failed to enable the disabled package [" + componentName.packageName + "]")
130             }
131             Log.i(TAG, "Successfully enabled package [${componentName.packageName}]")
132             launchApp(
133                 context,
134                 Intent(Intent.ACTION_MAIN).setComponent(componentName)
135                     .addCategory(Intent.CATEGORY_LAUNCHER).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
136             )
137         }
138 
139         companion object {
140             const val TAG = "DisabledAppLaunchProvider"
141         }
142     }
143 
144     /**
145      * Handles launching the system activity for Terms of Service (TOS) acceptance. This
146      * provider redirects the user to the TOS flow for apps restricted by TOS.
147      */
148     internal object TosDisabledAppLaunchProvider : AppLaunchProvider() {
launchnull149         override fun launch(context: Context, componentName: ComponentName, launchIntent: Intent?) {
150             val tosIntentName = context.resources.getString(R.string.user_tos_activity_intent)
151             try {
152                 launchApp(context, Intent.parseUri(tosIntentName, Intent.URI_ANDROID_APP_SCHEME))
153             } catch (se: URISyntaxException) {
154                 Log.e(TAG, "Invalid intent URI in user_tos_activity_intent", se)
155             }
156         }
157 
158         const val TAG = "TosDisabledAppLaunchProvider"
159     }
160 
161     /**
162      * Handles app mirroring scenarios. Assumes that a launchIntent is provided with the
163      * necessary information to launch the mirrored app.
164      */
165     internal object MirroringAppLaunchProvider :
166         AppLaunchProvider() {
launchnull167         override fun launch(context: Context, componentName: ComponentName, launchIntent: Intent?) {
168             launchIntent?.let {
169                 launchApp(context, it)
170             }
171         }
172     }
173 }
174