1/*
2 * Copyright (C) 2015 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
17package android.car;
18
19import android.os.Handler;
20import android.os.IBinder;
21import android.os.Looper;
22import android.os.RemoteException;
23
24import java.lang.ref.WeakReference;
25import java.util.HashMap;
26import java.util.Map;
27
28/**
29 * CarAppContextManager allows applications to set and listen for the current application context
30 * like active navigation or active voice command. Usually only one instance of such application
31 * should run in the system, and other app setting the flag for the matching app should
32 * lead into other app to stop.
33 * @hide
34 */
35public class CarAppContextManager implements CarManagerBase {
36    /**
37     * Listener to get notification for app getting information on app context change.
38     */
39    public interface AppContextChangeListener {
40        /**
41         * Application context has changed. Note that {@link CarAppContextManager} instance
42         * causing the change will not get this notification.
43         * @param activeContexts
44         */
45        void onAppContextChange(int activeContexts);
46    }
47
48    /**
49     * Listener to get notification for app getting information on app context ownership loss.
50     */
51    public interface AppContextOwnershipChangeListener {
52        /**
53         * Lost ownership for the context, which happens when other app has set the context.
54         * The app losing context should stop the action associated with the context.
55         * For example, navigaiton app currently running active navigation should stop navigation
56         * upon getting this for {@link CarAppContextManager#APP_CONTEXT_NAVIGATION}.
57         * @param context
58         */
59        void onAppContextOwnershipLoss(int context);
60    }
61
62    /** @hide */
63    public static final int APP_CONTEXT_START_FLAG = 0x1;
64    /**
65     * Flag for active navigation.
66     */
67    public static final int APP_CONTEXT_NAVIGATION = 0x1;
68    /**
69     * Flag for active voice command.
70     */
71    public static final int APP_CONTEXT_VOICE_COMMAND = 0x2;
72    /**
73     * Update this after adding a new flag.
74     * @hide
75     */
76    public static final int APP_CONTEXT_END_FLAG = 0x2;
77
78    private final IAppContext mService;
79    private final Handler mHandler;
80    private final IAppContextListenerImpl mBinderListener;
81    private final Map<Integer, AppContextOwnershipChangeListener> mOwnershipListeners;
82
83    private AppContextChangeListener mListener;
84    private int mContextFilter;
85
86    /**
87     * @hide
88     */
89    CarAppContextManager(IBinder service, Looper looper) {
90        mService = IAppContext.Stub.asInterface(service);
91        mHandler = new Handler(looper);
92        mBinderListener = new IAppContextListenerImpl(this);
93        mOwnershipListeners = new HashMap<Integer, AppContextOwnershipChangeListener>();
94    }
95
96    /**
97     * Register listener to monitor app context change. Only one listener can be registered and
98     * registering multiple times will lead into only the last listener to be active.
99     * @param listener
100     * @param contextFilter Flags of cotexts to get notification.
101     * @throws CarNotConnectedException
102     */
103    public void registerContextListener(AppContextChangeListener listener, int contextFilter)
104            throws CarNotConnectedException {
105        if (listener == null) {
106            throw new IllegalArgumentException("null listener");
107        }
108        synchronized(this) {
109            if (mListener == null || mContextFilter != contextFilter) {
110                try {
111                    mService.registerContextListener(mBinderListener, contextFilter);
112                } catch (RemoteException e) {
113                    throw new CarNotConnectedException(e);
114                }
115            }
116            mListener = listener;
117            mContextFilter = contextFilter;
118        }
119    }
120
121    /**
122     * Unregister listener and stop listening context change events. If app has owned a context
123     * by {@link #setActiveContext(int)}, it will be reset to inactive state.
124     * @throws CarNotConnectedException
125     */
126    public void unregisterContextListener() throws CarNotConnectedException {
127        synchronized(this) {
128            try {
129                mService.unregisterContextListener(mBinderListener);
130            } catch (RemoteException e) {
131                throw new CarNotConnectedException(e);
132            }
133            mListener = null;
134            mContextFilter = 0;
135        }
136    }
137
138    public int getActiveAppContexts() throws CarNotConnectedException {
139        try {
140            return mService.getActiveAppContexts();
141        } catch (RemoteException e) {
142            throw new CarNotConnectedException(e);
143        }
144    }
145
146    public boolean isOwningContext(int context) throws CarNotConnectedException {
147        try {
148            return mService.isOwningContext(mBinderListener, context);
149        } catch (RemoteException e) {
150            throw new CarNotConnectedException(e);
151        }
152    }
153
154    /**
155     * Set the given contexts as active. By setting this, the application is becoming owner
156     * of the context, and will get
157     * {@link AppContextOwnershipChangeListener#onAppContextOwnershipLoss(int)}
158     * if ownership is given to other app by calling this. Fore-ground app will have higher priority
159     * and other app cannot set the same context while owner is in fore-ground.
160     * Only one listener per context can be registered and
161     * registering multiple times will lead into only the last listener to be active.
162     * @param ownershipListener
163     * @param contexts
164     * @throws CarNotConnectedException
165     * @throws SecurityException If owner cannot be changed.
166     */
167    public void setActiveContexts(AppContextOwnershipChangeListener ownershipListener, int contexts)
168            throws SecurityException, CarNotConnectedException {
169        if (ownershipListener == null) {
170            throw new IllegalArgumentException("null listener");
171        }
172        synchronized (this) {
173            try {
174                mService.setActiveContexts(mBinderListener, contexts);
175            } catch (RemoteException e) {
176                throw new CarNotConnectedException(e);
177            }
178            for (int flag = APP_CONTEXT_START_FLAG; flag <= APP_CONTEXT_END_FLAG; flag <<= 1) {
179                if ((flag & contexts) != 0) {
180                    mOwnershipListeners.put(flag, ownershipListener);
181                }
182            }
183        }
184    }
185
186    /**
187     * Reset the given contexts, i.e. mark them as inactive. This also involves releasing ownership
188     * for the context.
189     * @param contexts
190     * @throws CarNotConnectedException
191     */
192    public void resetActiveContexts(int contexts) throws CarNotConnectedException {
193        try {
194            mService.resetActiveContexts(mBinderListener, contexts);
195        } catch (RemoteException e) {
196            throw new CarNotConnectedException(e);
197        }
198        synchronized (this) {
199            for (int flag = APP_CONTEXT_START_FLAG; flag <= APP_CONTEXT_END_FLAG; flag <<= 1) {
200                if ((flag & contexts) != 0) {
201                    mOwnershipListeners.remove(flag);
202                }
203            }
204        }
205    }
206
207    @Override
208    public void onCarDisconnected() {
209        // nothing to do
210    }
211
212    private void handleAppContextChange(int activeContexts) {
213        AppContextChangeListener listener;
214        int newContext;
215        synchronized (this) {
216            if (mListener == null) {
217                return;
218            }
219            listener = mListener;
220            newContext = activeContexts & mContextFilter;
221        }
222        listener.onAppContextChange(newContext);
223    }
224
225    private void handleAppContextOwnershipLoss(int context) {
226        AppContextOwnershipChangeListener listener;
227        synchronized (this) {
228            listener = mOwnershipListeners.get(context);
229            if (listener == null) {
230                return;
231            }
232        }
233        listener.onAppContextOwnershipLoss(context);
234    }
235
236    private static class IAppContextListenerImpl extends IAppContextListener.Stub {
237
238        private final WeakReference<CarAppContextManager> mManager;
239
240        private IAppContextListenerImpl(CarAppContextManager manager) {
241            mManager = new WeakReference<>(manager);
242        }
243
244        @Override
245        public void onAppContextChange(final int activeContexts) {
246            final CarAppContextManager manager = mManager.get();
247            if (manager == null) {
248                return;
249            }
250            manager.mHandler.post(new Runnable() {
251                @Override
252                public void run() {
253                    manager.handleAppContextChange(activeContexts);
254                }
255            });
256        }
257
258        @Override
259        public void onAppContextOwnershipLoss(final int context) {
260            final CarAppContextManager manager = mManager.get();
261            if (manager == null) {
262                return;
263            }
264            manager.mHandler.post(new Runnable() {
265                @Override
266                public void run() {
267                    manager.handleAppContextOwnershipLoss(context);
268                }
269            });
270        }
271    }
272}
273