1/*
2 * Copyright (C) 2013 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.camera.app;
18
19import android.app.Activity;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.pm.ActivityInfo;
23import android.content.res.Configuration;
24import android.graphics.Point;
25import android.os.Handler;
26import android.provider.Settings;
27import android.view.Display;
28import android.view.OrientationEventListener;
29import android.view.Surface;
30import android.view.WindowManager;
31
32import com.android.camera.debug.Log;
33import com.android.camera.util.AndroidServices;
34import com.android.camera.util.ApiHelper;
35import com.android.camera.util.CameraUtil;
36
37import java.util.ArrayList;
38import java.util.List;
39
40/**
41 * The implementation of {@link com.android.camera.app.OrientationManager}
42 * by {@link android.view.OrientationEventListener}.
43 */
44public class OrientationManagerImpl implements OrientationManager {
45    private static final Log.Tag TAG = new Log.Tag("OrientMgrImpl");
46
47    // DeviceOrientation hysteresis amount used in rounding, in degrees
48    private static final int ORIENTATION_HYSTERESIS = 5;
49
50    private final Activity mActivity;
51
52    // The handler used to invoke listener callback.
53    private final Handler mHandler;
54
55    private final MyOrientationEventListener mOrientationListener;
56
57    // We keep the last known orientation. So if the user first orient
58    // the camera then point the camera to floor or sky, we still have
59    // the correct orientation.
60    private DeviceOrientation mLastDeviceOrientation = DeviceOrientation.CLOCKWISE_0;
61
62    // If the framework orientation is locked.
63    private boolean mOrientationLocked = false;
64
65    // This is true if "Settings -> Display -> Rotation Lock" is checked. We
66    // don't allow the orientation to be unlocked if the value is true.
67    private boolean mRotationLockedSetting = false;
68
69    private final List<OnOrientationChangeListener> mListeners =
70            new ArrayList<OnOrientationChangeListener>();
71
72    private final boolean mIsDefaultToPortrait;
73
74    /**
75     * Instantiates a new orientation manager.
76     *
77     * @param activity The main activity object.
78     * @param handler The handler used to invoke listener callback.
79     */
80    public OrientationManagerImpl(Activity activity, Handler handler) {
81        mActivity = activity;
82        mOrientationListener = new MyOrientationEventListener(activity);
83        mHandler = handler;
84        mIsDefaultToPortrait = isDefaultToPortrait(activity);
85    }
86
87    public void resume() {
88        ContentResolver resolver = mActivity.getContentResolver();
89        mRotationLockedSetting = Settings.System.getInt(
90                resolver, Settings.System.ACCELEROMETER_ROTATION, 0) != 1;
91        mOrientationListener.enable();
92    }
93
94    public void pause() {
95        mOrientationListener.disable();
96    }
97
98    @Override
99    public DeviceNaturalOrientation getDeviceNaturalOrientation() {
100        return mIsDefaultToPortrait ? DeviceNaturalOrientation.PORTRAIT :
101                DeviceNaturalOrientation.LANDSCAPE;
102    }
103
104    @Override
105    public DeviceOrientation getDeviceOrientation() {
106        return mLastDeviceOrientation;
107    }
108
109    @Override
110    public DeviceOrientation getDisplayRotation() {
111        return DeviceOrientation.from((360 - CameraUtil.getDisplayRotation()) % 360);
112    }
113
114    @Override
115    public void addOnOrientationChangeListener(OnOrientationChangeListener listener) {
116        if (mListeners.contains(listener)) {
117            return;
118        }
119        mListeners.add(listener);
120    }
121
122    @Override
123    public void removeOnOrientationChangeListener(OnOrientationChangeListener listener) {
124        if (!mListeners.remove(listener)) {
125            Log.v(TAG, "Removing non-existing listener.");
126        }
127    }
128
129    @Override
130    public boolean isInLandscape() {
131        int roundedOrientationDegrees = mLastDeviceOrientation.getDegrees();
132        if (mIsDefaultToPortrait) {
133            if (roundedOrientationDegrees % 180 == 90) {
134                return true;
135            }
136        } else {
137            if (roundedOrientationDegrees % 180 == 0) {
138                return true;
139            }
140        }
141        return false;
142    }
143
144    @Override
145    public boolean isInPortrait() {
146        return !isInLandscape();
147    }
148
149    @Override
150    public void lockOrientation() {
151        if (mOrientationLocked || mRotationLockedSetting) {
152            return;
153        }
154        mOrientationLocked = true;
155        if (ApiHelper.HAS_ORIENTATION_LOCK) {
156            mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
157        } else {
158            mActivity.setRequestedOrientation(calculateCurrentScreenOrientation());
159        }
160    }
161
162    @Override
163    public void unlockOrientation() {
164        if (!mOrientationLocked || mRotationLockedSetting) {
165            return;
166        }
167        mOrientationLocked = false;
168        Log.d(TAG, "unlock orientation");
169        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
170    }
171
172    @Override
173    public boolean isOrientationLocked() {
174        return (mOrientationLocked || mRotationLockedSetting);
175    }
176
177    private int calculateCurrentScreenOrientation() {
178        int displayRotation = getDisplayRotation(mActivity);
179        // Display rotation >= 180 means we need to use the REVERSE landscape/portrait
180        boolean standard = displayRotation < 180;
181        if (mActivity.getResources().getConfiguration().orientation
182                == Configuration.ORIENTATION_LANDSCAPE) {
183            return standard
184                    ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
185                    : ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
186        } else {
187            if (displayRotation == 90 || displayRotation == 270) {
188                // If displayRotation = 90 or 270 then we are on a landscape
189                // device. On landscape devices, portrait is a 90 degree
190                // clockwise rotation from landscape, so we need
191                // to flip which portrait we pick as display rotation is counter clockwise
192                standard = !standard;
193            }
194            return standard
195                    ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
196                    : ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
197        }
198    }
199
200    // This listens to the device orientation, so we can update the compensation.
201    private class MyOrientationEventListener extends OrientationEventListener {
202        public MyOrientationEventListener(Context context) {
203            super(context);
204        }
205
206        @Override
207        public void onOrientationChanged(int orientation) {
208            if (orientation == ORIENTATION_UNKNOWN) {
209                return;
210            }
211
212            final DeviceOrientation roundedDeviceOrientation =
213                    roundOrientation(mLastDeviceOrientation, orientation);
214            if (roundedDeviceOrientation == mLastDeviceOrientation) {
215                return;
216            }
217            Log.v(TAG, "orientation changed (from:to) " + mLastDeviceOrientation +
218                    ":" + roundedDeviceOrientation);
219            mLastDeviceOrientation = roundedDeviceOrientation;
220
221            for (final OnOrientationChangeListener listener : mListeners) {
222                mHandler.post(new Runnable() {
223                    @Override
224                    public void run() {
225                        listener.onOrientationChanged(OrientationManagerImpl.this, roundedDeviceOrientation);
226                    }
227                });
228            }
229        }
230    }
231
232    private static DeviceOrientation roundOrientation(DeviceOrientation oldDeviceOrientation,
233                                                      int newRawOrientation) {
234        int dist = Math.abs(newRawOrientation - oldDeviceOrientation.getDegrees());
235        dist = Math.min(dist, 360 - dist);
236        boolean isOrientationChanged = (dist >= 45 + ORIENTATION_HYSTERESIS);
237
238        if (isOrientationChanged) {
239            int newRoundedOrientation = ((newRawOrientation + 45) / 90 * 90) % 360;
240            switch (newRoundedOrientation) {
241                case 0:
242                    return DeviceOrientation.CLOCKWISE_0;
243                case 90:
244                    return DeviceOrientation.CLOCKWISE_90;
245                case 180:
246                    return DeviceOrientation.CLOCKWISE_180;
247                case 270:
248                    return DeviceOrientation.CLOCKWISE_270;
249            }
250        }
251        return oldDeviceOrientation;
252    }
253
254    private static int getDisplayRotation(Activity activity) {
255        int rotation = activity.getWindowManager().getDefaultDisplay()
256                .getRotation();
257        switch (rotation) {
258            case Surface.ROTATION_0: return 0;
259            case Surface.ROTATION_90: return 90;
260            case Surface.ROTATION_180: return 180;
261            case Surface.ROTATION_270: return 270;
262        }
263        return 0;
264    }
265
266    /**
267     * Calculate the default orientation of the device based on the width and
268     * height of the display when rotation = 0 (i.e. natural width and height)
269     *
270     * @param context current context
271     * @return whether the default orientation of the device is portrait
272     */
273    private static boolean isDefaultToPortrait(Context context) {
274        Display currentDisplay = AndroidServices.instance().provideWindowManager()
275                .getDefaultDisplay();
276        Point displaySize = new Point();
277        currentDisplay.getSize(displaySize);
278        int orientation = currentDisplay.getRotation();
279        int naturalWidth, naturalHeight;
280        if (orientation == Surface.ROTATION_0 || orientation == Surface.ROTATION_180) {
281            naturalWidth = displaySize.x;
282            naturalHeight = displaySize.y;
283        } else {
284            naturalWidth = displaySize.y;
285            naturalHeight = displaySize.x;
286        }
287        return naturalWidth < naturalHeight;
288    }
289}
290