1 /* 2 * Copyright (C) 2018 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 android.car.cluster; 17 18 import android.annotation.Nullable; 19 import android.annotation.UiThread; 20 import android.app.ActivityManager; 21 import android.app.ActivityManager.StackInfo; 22 import android.app.IActivityManager; 23 import android.app.IProcessObserver; 24 import android.app.TaskStackListener; 25 import android.content.ComponentName; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.os.RemoteException; 29 import android.util.Log; 30 31 import java.util.HashMap; 32 import java.util.HashSet; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Set; 36 37 /** 38 * Top activity monitor, allows listeners to be notified when a new activity comes to the foreground 39 * on a particular device. 40 * 41 * As a sanity check {@link #notifyTopActivities} is handed to the UI thread because it is triggered 42 * by {@link #mProcessObserver} and {@link #mTaskStackListener}, which may be called by background 43 * threads. 44 * 45 * {@link #start} and {@link #stop} should be called only by the UI thread to prevent possible NPEs. 46 */ 47 public class ActivityMonitor { 48 private static final String TAG = "Cluster.ActivityMonitor"; 49 50 /** 51 * Listener of activity changes 52 */ 53 public interface ActivityListener { 54 /** 55 * Invoked when a new activity becomes the top activity on a particular display. 56 */ onTopActivityChanged(int displayId, @Nullable ComponentName activity)57 void onTopActivityChanged(int displayId, @Nullable ComponentName activity); 58 } 59 60 private IActivityManager mActivityManager; 61 // Listeners of top activity changes, indexed by the displayId they are interested on. 62 private final Map<Integer, Set<ActivityListener>> mListeners = new HashMap<>(); 63 private final Handler mHandler = new Handler(); 64 private final IProcessObserver.Stub mProcessObserver = new IProcessObserver.Stub() { 65 /** 66 * Note: This function may sometimes be called from a background thread 67 */ 68 @Override 69 public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) { 70 notifyTopActivities(); 71 } 72 73 /** 74 * Note: This function may sometimes be called from a background thread 75 */ 76 @Override 77 public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) { } 78 79 @Override 80 public void onProcessDied(int pid, int uid) { 81 notifyTopActivities(); 82 } 83 }; 84 private final TaskStackListener mTaskStackListener = new TaskStackListener() { 85 /** 86 * Note: This function may sometimes be called from a background thread 87 */ 88 @Override 89 public void onTaskStackChanged() { 90 Log.i(TAG, "onTaskStackChanged"); 91 notifyTopActivities(); 92 } 93 }; 94 95 /** 96 * Registers a new listener to receive activity updates on a particular display 97 * 98 * @param displayId identifier of the display to monitor 99 * @param listener listener to be notified 100 */ addListener(int displayId, ActivityListener listener)101 public void addListener(int displayId, ActivityListener listener) { 102 mListeners.computeIfAbsent(displayId, k -> new HashSet<>()).add(listener); 103 } 104 105 /** 106 * Unregisters a listener previously registered with {@link #addListener(int, ActivityListener)} 107 */ removeListener(int displayId, ActivityListener listener)108 public void removeListener(int displayId, ActivityListener listener) { 109 mListeners.computeIfAbsent(displayId, k -> new HashSet<>()).remove(listener); 110 } 111 112 /** 113 * Starts monitoring activity changes. {@link #stop()} should be invoked to release resources. 114 * 115 * This method should be called on the UI thread. Otherwise, runtime exceptions may occur. 116 */ 117 @UiThread start()118 public void start() { 119 mActivityManager = ActivityManager.getService(); 120 // Monitoring both listeners are necessary as there are cases where one listener cannot 121 // monitor activity change. 122 try { 123 mActivityManager.registerProcessObserver(mProcessObserver); 124 mActivityManager.registerTaskStackListener(mTaskStackListener); 125 } catch (RemoteException e) { 126 Log.e(TAG, "Cannot register activity monitoring", e); 127 throw new RuntimeException(e); 128 } 129 notifyTopActivities(); 130 } 131 132 /** 133 * Stops monitoring activity changes. Should be invoked when this monitor is not longer used. 134 * 135 * This method should be called on the UI thread. Otherwise, runtime exceptions may occur. 136 */ 137 @UiThread stop()138 public void stop() { 139 if (mActivityManager == null) { 140 return; 141 } 142 if (Looper.getMainLooper().getThread() != Thread.currentThread()) { 143 Log.w(TAG, "stop() is called on non-UI thread. May cause NPE"); 144 } 145 try { 146 mActivityManager.unregisterProcessObserver(mProcessObserver); 147 mActivityManager.unregisterTaskStackListener(mTaskStackListener); 148 } catch (RemoteException e) { 149 Log.e(TAG, "Cannot unregister activity monitoring. Ignoring", e); 150 } 151 mActivityManager = null; 152 } 153 154 /** 155 * Notifies listeners on changes of top activities. 156 * 157 * Note: This method may sometimes be called by background threads, so it is synchronized on 158 * the UI thread with mHandler.post() 159 */ notifyTopActivities()160 private void notifyTopActivities() { 161 mHandler.post(() -> { 162 try { 163 // return if the activity monitor is no longer used 164 if (mActivityManager == null) { 165 return; 166 } 167 List<StackInfo> infos = mActivityManager.getAllStackInfos(); 168 for (StackInfo info : infos) { 169 Set<ActivityListener> listeners = mListeners.get(info.displayId); 170 if (listeners != null && !listeners.isEmpty()) { 171 for (ActivityListener listener : listeners) { 172 listener.onTopActivityChanged(info.displayId, info.topActivity); 173 } 174 } 175 } 176 } catch (RemoteException e) { 177 Log.e(TAG, "Cannot getTasks", e); 178 } 179 }); 180 } 181 } 182