1 /* 2 * Copyright (C) 2016 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 package com.android.car.media; 17 18 import android.content.ComponentName; 19 import android.content.Intent; 20 import android.os.Bundle; 21 import android.provider.MediaStore; 22 import android.util.Log; 23 import com.android.car.app.CarDrawerActivity; 24 import com.android.car.app.CarDrawerAdapter; 25 import com.android.car.media.drawer.MediaDrawerController; 26 27 /** 28 * This activity controls the UI of media. It also updates the connection status for the media app 29 * by broadcast. Drawer menu is controlled by {@link MediaDrawerController}. 30 */ 31 public class MediaActivity extends CarDrawerActivity 32 implements MediaPlaybackFragment.PlayQueueRevealer { 33 private static final String ACTION_MEDIA_APP_STATE_CHANGE 34 = "android.intent.action.MEDIA_APP_STATE_CHANGE"; 35 private static final String EXTRA_MEDIA_APP_FOREGROUND 36 = "android.intent.action.MEDIA_APP_STATE"; 37 38 private static final String TAG = "MediaActivity"; 39 40 /** 41 * Whether or not {@link #onStart()} has been called. 42 */ 43 private boolean mIsStarted; 44 45 /** 46 * {@code true} if there was a request to change the content fragment of this Activity when 47 * it is not started. Then, when onStart() is called, the content fragment will be added. 48 * 49 * <p>This prevents a bug where the content fragment is added when the app is not running, 50 * causing a StateLossException. 51 */ 52 private boolean mContentFragmentChangeQueued; 53 54 private MediaDrawerController mDrawerController; 55 private MediaPlaybackFragment mMediaPlaybackFragment; 56 57 @Override 58 protected void onStart() { 59 super.onStart(); 60 Intent i = new Intent(ACTION_MEDIA_APP_STATE_CHANGE); 61 i.putExtra(EXTRA_MEDIA_APP_FOREGROUND, true); 62 sendBroadcast(i); 63 64 mIsStarted = true; 65 66 if (mContentFragmentChangeQueued) { 67 if (Log.isLoggable(TAG, Log.DEBUG)) { 68 Log.d(TAG, "Content fragment queued. Attaching now."); 69 } 70 showMediaPlaybackFragment(); 71 mContentFragmentChangeQueued = false; 72 } 73 } 74 75 @Override 76 protected void onStop() { 77 super.onStop(); 78 Intent i = new Intent(ACTION_MEDIA_APP_STATE_CHANGE); 79 i.putExtra(EXTRA_MEDIA_APP_FOREGROUND, false); 80 sendBroadcast(i); 81 82 mIsStarted = false; 83 } 84 85 @Override 86 protected void onCreate(Bundle savedInstanceState) { 87 // The drawer must be initialized before the super call because CarDrawerActivity.onCreate 88 // looks up the rootAdapter from its subclasses. The MediaDrawerController provides the 89 // root adapter. 90 mDrawerController = new MediaDrawerController(this); 91 92 super.onCreate(savedInstanceState); 93 94 setMainContent(R.layout.media_activity); 95 MediaManager.getInstance(this).addListener(mListener); 96 } 97 98 @Override 99 public void onDestroy() { 100 super.onDestroy(); 101 // Send the broadcast to let the current connected media app know it is disconnected now. 102 sendMediaConnectionStatusBroadcast( 103 MediaManager.getInstance(this).getCurrentComponent(), 104 MediaConstants.MEDIA_DISCONNECTED); 105 mDrawerController.cleanup(); 106 MediaManager.getInstance(this).removeListener(mListener); 107 mMediaPlaybackFragment = null; 108 } 109 110 @Override 111 protected CarDrawerAdapter getRootAdapter() { 112 return mDrawerController.getRootAdapter(); 113 } 114 115 @Override 116 public void onResumeFragments() { 117 if (Log.isLoggable(TAG, Log.DEBUG)) { 118 Log.d(TAG, "onResumeFragments"); 119 } 120 121 super.onResumeFragments(); 122 handleIntent(getIntent()); 123 } 124 125 @Override 126 protected void onNewIntent(Intent intent) { 127 super.onNewIntent(intent); 128 if (Log.isLoggable(TAG, Log.VERBOSE)) { 129 Log.v(TAG, "onNewIntent(); intent: " + (intent == null ? "<< NULL >>" : intent)); 130 } 131 132 setIntent(intent); 133 closeDrawer(); 134 } 135 136 @Override 137 public void onBackPressed() { 138 if (mMediaPlaybackFragment != null) { 139 mMediaPlaybackFragment.closeOverflowMenu(); 140 } 141 super.onBackPressed(); 142 } 143 144 private void handleIntent(Intent intent) { 145 Bundle extras = null; 146 if (intent != null) { 147 extras = intent.getExtras(); 148 } 149 150 // If the intent has a media component name set, connect to it directly 151 if (extras != null && extras.containsKey(MediaManager.KEY_MEDIA_PACKAGE) && 152 extras.containsKey(MediaManager.KEY_MEDIA_CLASS)) { 153 if (Log.isLoggable(TAG, Log.DEBUG)) { 154 Log.d(TAG, "Media component in intent."); 155 } 156 157 ComponentName component = new ComponentName( 158 intent.getStringExtra(MediaManager.KEY_MEDIA_PACKAGE), 159 intent.getStringExtra(MediaManager.KEY_MEDIA_CLASS) 160 ); 161 MediaManager.getInstance(this).setMediaClientComponent(component); 162 } else { 163 if (Log.isLoggable(TAG, Log.DEBUG)) { 164 Log.d(TAG, "Launching most recent / default component."); 165 } 166 167 // Set it to the default GPM component. 168 MediaManager.getInstance(this).connectToMostRecentMediaComponent( 169 new CarClientServiceAdapter(getPackageManager())); 170 } 171 172 if (isSearchIntent(intent)) { 173 MediaManager.getInstance(this).processSearchIntent(intent); 174 setIntent(null); 175 } 176 } 177 178 /** 179 * Returns {@code true} if the given intent is one that contains a search query for the 180 * attached media application. 181 */ 182 private boolean isSearchIntent(Intent intent) { 183 return (intent != null && intent.getAction() != null && 184 intent.getAction().equals(MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH)); 185 } 186 187 private void sendMediaConnectionStatusBroadcast(ComponentName componentName, 188 String connectionStatus) { 189 // There will be no current component if no media app has been chosen before. 190 if (componentName == null) { 191 return; 192 } 193 194 Intent intent = new Intent(MediaConstants.ACTION_MEDIA_STATUS); 195 intent.setPackage(componentName.getPackageName()); 196 intent.putExtra(MediaConstants.MEDIA_CONNECTION_STATUS, connectionStatus); 197 sendBroadcast(intent); 198 } 199 200 private void showMediaPlaybackFragment() { 201 // If the fragment has already been created, then it has been attached already. 202 if (mMediaPlaybackFragment != null) { 203 return; 204 } 205 206 mMediaPlaybackFragment = new MediaPlaybackFragment(); 207 mMediaPlaybackFragment.setPlayQueueRevealer(this); 208 209 getSupportFragmentManager().beginTransaction() 210 .replace(R.id.fragment_container, mMediaPlaybackFragment) 211 .commit(); 212 } 213 214 @Override 215 public void showPlayQueue() { 216 mDrawerController.showPlayQueue(); 217 } 218 219 private final MediaManager.Listener mListener = new MediaManager.Listener() { 220 @Override 221 public void onMediaAppChanged(ComponentName componentName) { 222 sendMediaConnectionStatusBroadcast(componentName, MediaConstants.MEDIA_CONNECTED); 223 224 // Since this callback happens asynchronously, ensure that the Activity has been 225 // started before changing fragments. Otherwise, the attach fragment will throw 226 // an IllegalStateException due to Fragment's checkStateLoss. 227 if (mIsStarted) { 228 if (Log.isLoggable(TAG, Log.DEBUG)) { 229 Log.d(TAG, "onMediaAppChanged: attaching content fragment"); 230 } 231 showMediaPlaybackFragment(); 232 } else { 233 if (Log.isLoggable(TAG, Log.DEBUG)) { 234 Log.d(TAG, "onMediaAppChanged: queuing content fragment change"); 235 } 236 mContentFragmentChangeQueued = true; 237 } 238 } 239 240 @Override 241 public void onStatusMessageChanged(String msg) {} 242 }; 243 } 244