1/*
2 * Copyright (C) 2014 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 com.android.ex.camera2.utils;
18
19import android.hardware.camera2.CameraAccessException;
20import android.hardware.camera2.CameraDevice;
21import android.hardware.camera2.CaptureRequest;
22import android.hardware.camera2.CaptureRequest.Builder;
23import android.hardware.camera2.CaptureRequest.Key;
24import android.view.Surface;
25
26import java.util.HashMap;
27import java.util.Map;
28import java.util.Objects;
29
30/**
31 * A set of settings to be used when filing a {@link CaptureRequest}.
32 */
33public class Camera2RequestSettingsSet {
34    private final Map<Key<?>, Object> mDictionary;
35    private long mRevision;
36
37    /**
38     * Create a new instance with no settings defined.
39     *
40     * <p>Creating a request from this object without first specifying any
41     * properties on it is equivalent to just creating a request directly
42     * from the template of choice. Its revision identifier is initially
43     * {@code 0}, and will remain thus until its first modification.</p>
44     */
45    public Camera2RequestSettingsSet() {
46        mDictionary = new HashMap<>();
47        mRevision = 0;
48    }
49
50    /**
51     * Perform a deep copy of the defined settings and revision number.
52     *
53     * @param other The reference instance.
54     *
55     * @throws NullPointerException If {@code other} is {@code null}.
56     */
57    public Camera2RequestSettingsSet(Camera2RequestSettingsSet other) {
58        if (other == null) {
59            throw new NullPointerException("Tried to copy null Camera2RequestSettingsSet");
60        }
61
62        mDictionary = new HashMap<>(other.mDictionary);
63        mRevision = other.mRevision;
64    }
65
66    /**
67     * Specify a setting, potentially overriding the template's default choice.
68     *
69     * <p>Providing a {@code null} {@code value} will indicate a forced use of
70     * the template's selection for that {@code key}; the difference here is
71     * that this information will be propagated with unions as documented in
72     * {@link #union}. This method increments the revision identifier if the new
73     * choice is different than the existing selection.</p>
74     *
75     * @param key Which setting to alter.
76     * @param value The new selection for that setting, or {@code null} to force
77     *              the use of the template's default selection for this field.
78     * @return Whether the settings were updated, which only occurs if the
79     *         {@code value} is different from any already stored.
80     *
81     * @throws NullPointerException If {@code key} is {@code null}.
82     */
83    public <T> boolean set(Key<T> key, T value) {
84        if (key == null) {
85            throw new NullPointerException("Received a null key");
86        }
87
88        Object currentValue = get(key);
89        // Only save the value if it's different from the one we already have
90        if (!mDictionary.containsKey(key) || !Objects.equals(value, currentValue)) {
91            mDictionary.put(key, value);
92            ++mRevision;
93            return true;
94        }
95        return false;
96    }
97
98    /**
99     * Unsets a setting, preventing it from being propagated with unions or from
100     * overriding the default when creating a capture request.
101     *
102     * <p>This method increments the revision identifier if a selection had
103     * previously been made for that parameter.</p>
104     *
105     * @param key Which setting to reset.
106     * @return Whether the settings were updated, which only occurs if the
107     *         specified setting already had a value or was forced to default.
108     *
109     * @throws NullPointerException If {@code key} is {@code null}.
110     */
111    public boolean unset(Key<?> key) {
112        if (key == null) {
113            throw new NullPointerException("Received a null key");
114        }
115
116        if (mDictionary.containsKey(key)) {
117            mDictionary.remove(key);
118            ++mRevision;
119            return true;
120        }
121        return false;
122    }
123
124    /**
125     * Interrogate the current specialization of a setting.
126     *
127     * @param key Which setting to check.
128     * @return The current selection for that setting, or {@code null} if the
129     *         setting is unset or forced to the template-defined default.
130     *
131     * @throws NullPointerException If {@code key} is {@code null}.
132     */
133    @SuppressWarnings("unchecked")
134    public <T> T get(Key<T> key) {
135        if (key == null) {
136            throw new NullPointerException("Received a null key");
137        }
138        return (T) mDictionary.get(key);
139    }
140
141    /**
142     * Query this instance for whether it prefers a particular choice for the
143     * given request parameter.
144     *
145     * <p>This method can be used to detect whether a particular field is forced
146     * to its default value or simply unset. While {@link #get} will return
147     * {@code null} in both these cases, this method will return {@code true}
148     * and {@code false}, respectively.</p>
149     *
150     * @param key Which setting to look for.
151     * @return Whether that setting has a value that will propagate with unions.
152     *
153     * @throws NullPointerException If {@code key} is {@code null}.
154     */
155    public boolean contains(Key<?> key) {
156        if (key == null) {
157            throw new NullPointerException("Received a null key");
158        }
159        return mDictionary.containsKey(key);
160    }
161
162    /**
163     * Check whether the value of the specified setting matches the given one.
164     *
165     * <p>This method uses the {@code T} type's {@code equals} method, but is
166     * {@code null}-tolerant.</p>
167     *
168     * @param key Which of this class's settings to check.
169     * @param value Value to test for equality against.
170     * @return Whether they are the same.
171     */
172    public <T> boolean matches(Key<T> key, T value) {
173        return Objects.equals(get(key), value);
174    }
175
176    /**
177     * Get this set of settings's revision identifier, which can be compared
178     * against cached past values to determine whether it has been modified.
179     *
180     * <p>Distinct revisions across the same object do not necessarily indicate
181     * that the object's key/value pairs have changed at all, but the same
182     * revision on the same object does imply that they've stayed the same.</p>
183     *
184     * @return The number of modifications made since the beginning of this
185     *         object's heritage.
186     */
187    public long getRevision() {
188        return mRevision;
189    }
190
191    /**
192     * Add all settings choices defined by {@code moreSettings} to this object.
193     *
194     * <p>For any settings defined in both, the choice stored in the argument
195     * to this method take precedence. Unset settings are not propagated, but
196     * those forced to default as described in {@link set} are also forced to
197     * default in {@code this} set. Invoking this method increments {@code this}
198     * object's revision counter, but leaves the argument's unchanged.</p>
199     *
200     * @param moreSettings The source of the additional settings ({@code null}
201     *                     is allowed here).
202     * @return Whether these settings were updated, which can only fail if the
203     *         target itself is also given as the argument.
204     */
205    public boolean union(Camera2RequestSettingsSet moreSettings) {
206        if (moreSettings == null || moreSettings == this) {
207            return false;
208        }
209
210        mDictionary.putAll(moreSettings.mDictionary);
211        ++mRevision;
212        return true;
213    }
214
215    /**
216     * Create a {@link CaptureRequest} specialized for the specified
217     * {@link CameraDevice} and targeting the given {@link Surface}s.
218     *
219     * @param camera The camera from which to capture.
220     * @param template A {@link CaptureRequest} template defined in
221     *                 {@link CameraDevice}.
222     * @param targets The location(s) to draw the resulting image onto.
223     * @return The request, ready to be passed to the camera framework.
224     *
225     * @throws CameraAccessException Upon an underlying framework API failure.
226     * @throws NullPointerException If any argument is {@code null}.
227     */
228    public CaptureRequest createRequest(CameraDevice camera, int template, Surface... targets)
229            throws CameraAccessException {
230        if (camera == null) {
231            throw new NullPointerException("Tried to create request using null CameraDevice");
232        }
233
234        Builder reqBuilder = camera.createCaptureRequest(template);
235        for (Key<?> key : mDictionary.keySet()) {
236            setRequestFieldIfNonNull(reqBuilder, key);
237        }
238        for (Surface target : targets) {
239            if (target == null) {
240                throw new NullPointerException("Tried to add null Surface as request target");
241            }
242            reqBuilder.addTarget(target);
243        }
244        return reqBuilder.build();
245    }
246
247    private <T> void setRequestFieldIfNonNull(Builder requestBuilder, Key<T> key) {
248        T value = get(key);
249        if (value != null) {
250            requestBuilder.set(key, value);
251        }
252    }
253}
254