1/*
2 * Copyright (C) 2017 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 */
16package androidx.wear.ambient;
17
18import android.app.Activity;
19import android.content.Context;
20import android.os.Bundle;
21import android.util.Log;
22
23import androidx.annotation.CallSuper;
24import androidx.annotation.Nullable;
25import androidx.annotation.VisibleForTesting;
26import androidx.fragment.app.Fragment;
27import androidx.fragment.app.FragmentActivity;
28import androidx.fragment.app.FragmentManager;
29
30import com.google.android.wearable.compat.WearableActivityController;
31
32import java.io.FileDescriptor;
33import java.io.PrintWriter;
34
35/**
36 * Use this as a headless Fragment to add ambient support to an Activity on Wearable devices.
37 * <p>
38 * The application that uses this should add the {@link android.Manifest.permission#WAKE_LOCK}
39 * permission to its manifest.
40 * <p>
41 * The primary entry  point for this code is the {@link #attach(FragmentActivity)} method.
42 * It should be called with an {@link FragmentActivity} as an argument and that
43 * {@link FragmentActivity} will then be able to receive ambient lifecycle events through
44 * an {@link AmbientCallback}. The {@link FragmentActivity} will also receive a
45 * {@link AmbientController} object from the attachment which can be used to query the current
46 * status of the ambient mode. An example of how to attach {@link AmbientModeSupport} to your
47 * {@link FragmentActivity} and use the {@link AmbientController} can be found below:
48 * <p>
49 * <pre class="prettyprint">{@code
50 *     AmbientMode.AmbientController controller = AmbientMode.attachAmbientSupport(this);
51 *     boolean isAmbient =  controller.isAmbient();
52 * }</pre>
53 */
54public final class AmbientModeSupport extends Fragment {
55    private static final String TAG = "AmbientMode";
56
57    /**
58     * Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate
59     * whether burn-in protection is required. When this property is set to true, views must be
60     * shifted around periodically in ambient mode. To ensure that content isn't shifted off
61     * the screen, avoid placing content within 10 pixels of the edge of the screen. Activities
62     * should also avoid solid white areas to prevent pixel burn-in. Both of these requirements
63     * only apply in ambient mode, and only when this property is set to true.
64     */
65    public static final String EXTRA_BURN_IN_PROTECTION =
66            WearableActivityController.EXTRA_BURN_IN_PROTECTION;
67
68    /**
69     * Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate
70     * whether the device has low-bit ambient mode. When this property is set to true, the screen
71     * supports fewer bits for each color in ambient mode. In this case, activities should disable
72     * anti-aliasing in ambient mode.
73     */
74    public static final String EXTRA_LOWBIT_AMBIENT =
75            WearableActivityController.EXTRA_LOWBIT_AMBIENT;
76
77    /**
78     * Fragment tag used by default when adding {@link AmbientModeSupport} to add ambient support to
79     * a {@link FragmentActivity}.
80     */
81    public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
82
83    /**
84     * Interface for any {@link Activity} that wishes to implement Ambient Mode. Use the
85     * {@link #getAmbientCallback()} method to return and {@link AmbientCallback} which can be used
86     * to bind the {@link AmbientModeSupport} to the instantiation of this interface.
87     * <p>
88     * <pre class="prettyprint">{@code
89     * return new AmbientMode.AmbientCallback() {
90     *     public void onEnterAmbient(Bundle ambientDetails) {...}
91     *     public void onExitAmbient(Bundle ambientDetails) {...}
92     *  }
93     * }</pre>
94     */
95    public interface AmbientCallbackProvider {
96        /**
97         * @return the {@link AmbientCallback} to be used by this class to communicate with the
98         * entity interested in ambient events.
99         */
100        AmbientCallback getAmbientCallback();
101    }
102
103    /**
104     * Callback to receive ambient mode state changes. It must be used by all users of AmbientMode.
105     */
106    public abstract static class AmbientCallback {
107        /**
108         * Called when an activity is entering ambient mode. This event is sent while an activity is
109         * running (after onResume, before onPause). All drawing should complete by the conclusion
110         * of this method. Note that {@code invalidate()} calls will be executed before resuming
111         * lower-power mode.
112         *
113         * @param ambientDetails bundle containing information about the display being used.
114         *                      It includes information about low-bit color and burn-in protection.
115         */
116        public void onEnterAmbient(Bundle ambientDetails) {}
117
118        /**
119         * Called when the system is updating the display for ambient mode. Activities may use this
120         * opportunity to update or invalidate views.
121         */
122        public void onUpdateAmbient() {}
123
124        /**
125         * Called when an activity should exit ambient mode. This event is sent while an activity is
126         * running (after onResume, before onPause).
127         */
128        public void onExitAmbient() {}
129    }
130
131    private final AmbientDelegate.AmbientCallback mCallback =
132            new AmbientDelegate.AmbientCallback() {
133                @Override
134                public void onEnterAmbient(Bundle ambientDetails) {
135                    if (mSuppliedCallback != null) {
136                        mSuppliedCallback.onEnterAmbient(ambientDetails);
137                    }
138                }
139
140                @Override
141                public void onExitAmbient() {
142                    if (mSuppliedCallback != null) {
143                        mSuppliedCallback.onExitAmbient();
144                    }
145                }
146
147                @Override
148                public void onUpdateAmbient() {
149                    if (mSuppliedCallback != null) {
150                        mSuppliedCallback.onUpdateAmbient();
151                    }
152                }
153            };
154    private AmbientDelegate mDelegate;
155    @Nullable
156    private AmbientCallback mSuppliedCallback;
157    private AmbientController mController;
158
159    /**
160     * Constructor
161     */
162    public AmbientModeSupport() {
163        mController = new AmbientController();
164    }
165
166    @Override
167    @CallSuper
168    public void onAttach(Context context) {
169        super.onAttach(context);
170        mDelegate = new AmbientDelegate(getActivity(), new WearableControllerProvider(), mCallback);
171
172        if (context instanceof AmbientCallbackProvider) {
173            mSuppliedCallback = ((AmbientCallbackProvider) context).getAmbientCallback();
174        } else {
175            Log.w(TAG, "No callback provided - enabling only smart resume");
176        }
177    }
178
179    @Override
180    @CallSuper
181    public void onCreate(Bundle savedInstanceState) {
182        super.onCreate(savedInstanceState);
183        mDelegate.onCreate();
184        if (mSuppliedCallback != null) {
185            mDelegate.setAmbientEnabled();
186        }
187    }
188
189    @Override
190    @CallSuper
191    public void onResume() {
192        super.onResume();
193        mDelegate.onResume();
194    }
195
196    @Override
197    @CallSuper
198    public void onPause() {
199        mDelegate.onPause();
200        super.onPause();
201    }
202
203    @Override
204    @CallSuper
205    public void onStop() {
206        mDelegate.onStop();
207        super.onStop();
208    }
209
210    @Override
211    @CallSuper
212    public void onDestroy() {
213        mDelegate.onDestroy();
214        super.onDestroy();
215    }
216
217    @Override
218    @CallSuper
219    public void onDetach() {
220        mDelegate = null;
221        super.onDetach();
222    }
223
224    /**
225     * Attach ambient support to the given activity. Calling this method with an Activity
226     * implementing the {@link AmbientCallbackProvider} interface will provide you with an
227     * opportunity to react to ambient events such as {@code onEnterAmbient}. Alternatively,
228     * you can call this method with an Activity which does not implement
229     * the {@link AmbientCallbackProvider} interface and that will only enable the auto-resume
230     * functionality. This is equivalent to providing (@code null} from
231     * the {@link AmbientCallbackProvider}.
232     *
233     * @param activity the activity to attach ambient support to.
234     * @return the associated {@link AmbientController} which can be used to query the state of
235     * ambient mode.
236     */
237    public static <T extends FragmentActivity> AmbientController attach(T activity) {
238        FragmentManager fragmentManager = activity.getSupportFragmentManager();
239        AmbientModeSupport ambientFragment =
240                (AmbientModeSupport) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
241        if (ambientFragment == null) {
242            AmbientModeSupport fragment = new AmbientModeSupport();
243            fragmentManager
244                    .beginTransaction()
245                    .add(fragment, FRAGMENT_TAG)
246                    .commit();
247            ambientFragment = fragment;
248        }
249        return ambientFragment.mController;
250    }
251
252    @Override
253    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
254        if (mDelegate != null) {
255            mDelegate.dump(prefix, fd, writer, args);
256        }
257    }
258
259    @VisibleForTesting
260    void setAmbientDelegate(AmbientDelegate delegate) {
261        mDelegate = delegate;
262    }
263
264    /**
265     * A class for interacting with the ambient mode on a wearable device. This class can be used to
266     * query the current state of ambient mode. An instance of this class is returned to the user
267     * when they attach their {@link Activity} to {@link AmbientModeSupport}.
268     */
269    public final class AmbientController {
270        private static final String TAG = "AmbientController";
271
272        // Do not initialize outside of this class.
273        AmbientController() {}
274
275        /**
276         * @return {@code true} if the activity is currently in ambient.
277         */
278        public boolean isAmbient() {
279            return mDelegate == null ? false : mDelegate.isAmbient();
280        }
281    }
282}
283