1/*
2 * Copyright (C) 2006 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.content.res;
18
19import android.content.pm.ApplicationInfo;
20import android.graphics.Canvas;
21import android.graphics.PointF;
22import android.graphics.Rect;
23import android.graphics.Region;
24import android.os.Build;
25import android.os.Build.VERSION_CODES;
26import android.os.Parcel;
27import android.os.Parcelable;
28import android.util.DisplayMetrics;
29import android.view.MotionEvent;
30import android.view.WindowManager;
31import android.view.WindowManager.LayoutParams;
32
33/**
34 * CompatibilityInfo class keeps the information about compatibility mode that the application is
35 * running under.
36 *
37 *  {@hide}
38 */
39public class CompatibilityInfo implements Parcelable {
40    /** default compatibility info object for compatible applications */
41    public static final CompatibilityInfo DEFAULT_COMPATIBILITY_INFO = new CompatibilityInfo() {
42    };
43
44    /**
45     * This is the number of pixels we would like to have along the
46     * short axis of an app that needs to run on a normal size screen.
47     */
48    public static final int DEFAULT_NORMAL_SHORT_DIMENSION = 320;
49
50    /**
51     * This is the maximum aspect ratio we will allow while keeping
52     * applications in a compatible screen size.
53     */
54    public static final float MAXIMUM_ASPECT_RATIO = (854f/480f);
55
56    /**
57     *  A compatibility flags
58     */
59    private final int mCompatibilityFlags;
60
61    /**
62     * A flag mask to tell if the application needs scaling (when mApplicationScale != 1.0f)
63     * {@see compatibilityFlag}
64     */
65    private static final int SCALING_REQUIRED = 1;
66
67    /**
68     * Application must always run in compatibility mode?
69     */
70    private static final int ALWAYS_NEEDS_COMPAT = 2;
71
72    /**
73     * Application never should run in compatibility mode?
74     */
75    private static final int NEVER_NEEDS_COMPAT = 4;
76
77    /**
78     * Set if the application needs to run in screen size compatibility mode.
79     */
80    private static final int NEEDS_SCREEN_COMPAT = 8;
81
82    /**
83     * Set if the application needs to run in with compat resources.
84     */
85    private static final int NEEDS_COMPAT_RES = 16;
86
87    /**
88     * The effective screen density we have selected for this application.
89     */
90    public final int applicationDensity;
91
92    /**
93     * Application's scale.
94     */
95    public final float applicationScale;
96
97    /**
98     * Application's inverted scale.
99     */
100    public final float applicationInvertedScale;
101
102    public CompatibilityInfo(ApplicationInfo appInfo, int screenLayout, int sw,
103            boolean forceCompat) {
104        int compatFlags = 0;
105
106        if (appInfo.targetSdkVersion < VERSION_CODES.O) {
107            compatFlags |= NEEDS_COMPAT_RES;
108        }
109        if (appInfo.requiresSmallestWidthDp != 0 || appInfo.compatibleWidthLimitDp != 0
110                || appInfo.largestWidthLimitDp != 0) {
111            // New style screen requirements spec.
112            int required = appInfo.requiresSmallestWidthDp != 0
113                    ? appInfo.requiresSmallestWidthDp
114                    : appInfo.compatibleWidthLimitDp;
115            if (required == 0) {
116                required = appInfo.largestWidthLimitDp;
117            }
118            int compat = appInfo.compatibleWidthLimitDp != 0
119                    ? appInfo.compatibleWidthLimitDp : required;
120            if (compat < required)  {
121                compat = required;
122            }
123            int largest = appInfo.largestWidthLimitDp;
124
125            if (required > DEFAULT_NORMAL_SHORT_DIMENSION) {
126                // For now -- if they require a size larger than the only
127                // size we can do in compatibility mode, then don't ever
128                // allow the app to go in to compat mode.  Trying to run
129                // it at a smaller size it can handle will make it far more
130                // broken than running at a larger size than it wants or
131                // thinks it can handle.
132                compatFlags |= NEVER_NEEDS_COMPAT;
133            } else if (largest != 0 && sw > largest) {
134                // If the screen size is larger than the largest size the
135                // app thinks it can work with, then always force it in to
136                // compatibility mode.
137                compatFlags |= NEEDS_SCREEN_COMPAT | ALWAYS_NEEDS_COMPAT;
138            } else if (compat >= sw) {
139                // The screen size is something the app says it was designed
140                // for, so never do compatibility mode.
141                compatFlags |= NEVER_NEEDS_COMPAT;
142            } else if (forceCompat) {
143                // The app may work better with or without compatibility mode.
144                // Let the user decide.
145                compatFlags |= NEEDS_SCREEN_COMPAT;
146            }
147
148            // Modern apps always support densities.
149            applicationDensity = DisplayMetrics.DENSITY_DEVICE;
150            applicationScale = 1.0f;
151            applicationInvertedScale = 1.0f;
152
153        } else {
154            /**
155             * Has the application said that its UI is expandable?  Based on the
156             * <supports-screen> android:expandible in the manifest.
157             */
158            final int EXPANDABLE = 2;
159
160            /**
161             * Has the application said that its UI supports large screens?  Based on the
162             * <supports-screen> android:largeScreens in the manifest.
163             */
164            final int LARGE_SCREENS = 8;
165
166            /**
167             * Has the application said that its UI supports xlarge screens?  Based on the
168             * <supports-screen> android:xlargeScreens in the manifest.
169             */
170            final int XLARGE_SCREENS = 32;
171
172            int sizeInfo = 0;
173
174            // We can't rely on the application always setting
175            // FLAG_RESIZEABLE_FOR_SCREENS so will compute it based on various input.
176            boolean anyResizeable = false;
177
178            if ((appInfo.flags & ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS) != 0) {
179                sizeInfo |= LARGE_SCREENS;
180                anyResizeable = true;
181                if (!forceCompat) {
182                    // If we aren't forcing the app into compatibility mode, then
183                    // assume if it supports large screens that we should allow it
184                    // to use the full space of an xlarge screen as well.
185                    sizeInfo |= XLARGE_SCREENS | EXPANDABLE;
186                }
187            }
188            if ((appInfo.flags & ApplicationInfo.FLAG_SUPPORTS_XLARGE_SCREENS) != 0) {
189                anyResizeable = true;
190                if (!forceCompat) {
191                    sizeInfo |= XLARGE_SCREENS | EXPANDABLE;
192                }
193            }
194            if ((appInfo.flags & ApplicationInfo.FLAG_RESIZEABLE_FOR_SCREENS) != 0) {
195                anyResizeable = true;
196                sizeInfo |= EXPANDABLE;
197            }
198
199            if (forceCompat) {
200                // If we are forcing compatibility mode, then ignore an app that
201                // just says it is resizable for screens.  We'll only have it fill
202                // the screen if it explicitly says it supports the screen size we
203                // are running in.
204                sizeInfo &= ~EXPANDABLE;
205            }
206
207            compatFlags |= NEEDS_SCREEN_COMPAT;
208            switch (screenLayout&Configuration.SCREENLAYOUT_SIZE_MASK) {
209                case Configuration.SCREENLAYOUT_SIZE_XLARGE:
210                    if ((sizeInfo&XLARGE_SCREENS) != 0) {
211                        compatFlags &= ~NEEDS_SCREEN_COMPAT;
212                    }
213                    if ((appInfo.flags & ApplicationInfo.FLAG_SUPPORTS_XLARGE_SCREENS) != 0) {
214                        compatFlags |= NEVER_NEEDS_COMPAT;
215                    }
216                    break;
217                case Configuration.SCREENLAYOUT_SIZE_LARGE:
218                    if ((sizeInfo&LARGE_SCREENS) != 0) {
219                        compatFlags &= ~NEEDS_SCREEN_COMPAT;
220                    }
221                    if ((appInfo.flags & ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS) != 0) {
222                        compatFlags |= NEVER_NEEDS_COMPAT;
223                    }
224                    break;
225            }
226
227            if ((screenLayout&Configuration.SCREENLAYOUT_COMPAT_NEEDED) != 0) {
228                if ((sizeInfo&EXPANDABLE) != 0) {
229                    compatFlags &= ~NEEDS_SCREEN_COMPAT;
230                } else if (!anyResizeable) {
231                    compatFlags |= ALWAYS_NEEDS_COMPAT;
232                }
233            } else {
234                compatFlags &= ~NEEDS_SCREEN_COMPAT;
235                compatFlags |= NEVER_NEEDS_COMPAT;
236            }
237
238            if ((appInfo.flags & ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES) != 0) {
239                applicationDensity = DisplayMetrics.DENSITY_DEVICE;
240                applicationScale = 1.0f;
241                applicationInvertedScale = 1.0f;
242            } else {
243                applicationDensity = DisplayMetrics.DENSITY_DEFAULT;
244                applicationScale = DisplayMetrics.DENSITY_DEVICE
245                        / (float) DisplayMetrics.DENSITY_DEFAULT;
246                applicationInvertedScale = 1.0f / applicationScale;
247                compatFlags |= SCALING_REQUIRED;
248            }
249        }
250
251        mCompatibilityFlags = compatFlags;
252    }
253
254    private CompatibilityInfo(int compFlags,
255            int dens, float scale, float invertedScale) {
256        mCompatibilityFlags = compFlags;
257        applicationDensity = dens;
258        applicationScale = scale;
259        applicationInvertedScale = invertedScale;
260    }
261
262    private CompatibilityInfo() {
263        this(NEVER_NEEDS_COMPAT, DisplayMetrics.DENSITY_DEVICE,
264                1.0f,
265                1.0f);
266    }
267
268    /**
269     * @return true if the scaling is required
270     */
271    public boolean isScalingRequired() {
272        return (mCompatibilityFlags&SCALING_REQUIRED) != 0;
273    }
274
275    public boolean supportsScreen() {
276        return (mCompatibilityFlags&NEEDS_SCREEN_COMPAT) == 0;
277    }
278
279    public boolean neverSupportsScreen() {
280        return (mCompatibilityFlags&ALWAYS_NEEDS_COMPAT) != 0;
281    }
282
283    public boolean alwaysSupportsScreen() {
284        return (mCompatibilityFlags&NEVER_NEEDS_COMPAT) != 0;
285    }
286
287    public boolean needsCompatResources() {
288        return (mCompatibilityFlags&NEEDS_COMPAT_RES) != 0;
289    }
290
291    /**
292     * Returns the translator which translates the coordinates in compatibility mode.
293     * @param params the window's parameter
294     */
295    public Translator getTranslator() {
296        return isScalingRequired() ? new Translator() : null;
297    }
298
299    /**
300     * A helper object to translate the screen and window coordinates back and forth.
301     * @hide
302     */
303    public class Translator {
304        final public float applicationScale;
305        final public float applicationInvertedScale;
306
307        private Rect mContentInsetsBuffer = null;
308        private Rect mVisibleInsetsBuffer = null;
309        private Region mTouchableAreaBuffer = null;
310
311        Translator(float applicationScale, float applicationInvertedScale) {
312            this.applicationScale = applicationScale;
313            this.applicationInvertedScale = applicationInvertedScale;
314        }
315
316        Translator() {
317            this(CompatibilityInfo.this.applicationScale,
318                    CompatibilityInfo.this.applicationInvertedScale);
319        }
320
321        /**
322         * Translate the screen rect to the application frame.
323         */
324        public void translateRectInScreenToAppWinFrame(Rect rect) {
325            rect.scale(applicationInvertedScale);
326        }
327
328        /**
329         * Translate the region in window to screen.
330         */
331        public void translateRegionInWindowToScreen(Region transparentRegion) {
332            transparentRegion.scale(applicationScale);
333        }
334
335        /**
336         * Apply translation to the canvas that is necessary to draw the content.
337         */
338        public void translateCanvas(Canvas canvas) {
339            if (applicationScale == 1.5f) {
340                /*  When we scale for compatibility, we can put our stretched
341                    bitmaps and ninepatches on exacty 1/2 pixel boundaries,
342                    which can give us inconsistent drawing due to imperfect
343                    float precision in the graphics engine's inverse matrix.
344
345                    As a work-around, we translate by a tiny amount to avoid
346                    landing on exact pixel centers and boundaries, giving us
347                    the slop we need to draw consistently.
348
349                    This constant is meant to resolve to 1/255 after it is
350                    scaled by 1.5 (applicationScale). Note, this is just a guess
351                    as to what is small enough not to create its own artifacts,
352                    and big enough to avoid the precision problems. Feel free
353                    to experiment with smaller values as you choose.
354                 */
355                final float tinyOffset = 2.0f / (3 * 255);
356                canvas.translate(tinyOffset, tinyOffset);
357            }
358            canvas.scale(applicationScale, applicationScale);
359        }
360
361        /**
362         * Translate the motion event captured on screen to the application's window.
363         */
364        public void translateEventInScreenToAppWindow(MotionEvent event) {
365            event.scale(applicationInvertedScale);
366        }
367
368        /**
369         * Translate the window's layout parameter, from application's view to
370         * Screen's view.
371         */
372        public void translateWindowLayout(WindowManager.LayoutParams params) {
373            params.scale(applicationScale);
374        }
375
376        /**
377         * Translate a Rect in application's window to screen.
378         */
379        public void translateRectInAppWindowToScreen(Rect rect) {
380            rect.scale(applicationScale);
381        }
382
383        /**
384         * Translate a Rect in screen coordinates into the app window's coordinates.
385         */
386        public void translateRectInScreenToAppWindow(Rect rect) {
387            rect.scale(applicationInvertedScale);
388        }
389
390        /**
391         * Translate a Point in screen coordinates into the app window's coordinates.
392         */
393        public void translatePointInScreenToAppWindow(PointF point) {
394            final float scale = applicationInvertedScale;
395            if (scale != 1.0f) {
396                point.x *= scale;
397                point.y *= scale;
398            }
399        }
400
401        /**
402         * Translate the location of the sub window.
403         * @param params
404         */
405        public void translateLayoutParamsInAppWindowToScreen(LayoutParams params) {
406            params.scale(applicationScale);
407        }
408
409        /**
410         * Translate the content insets in application window to Screen. This uses
411         * the internal buffer for content insets to avoid extra object allocation.
412         */
413        public Rect getTranslatedContentInsets(Rect contentInsets) {
414            if (mContentInsetsBuffer == null) mContentInsetsBuffer = new Rect();
415            mContentInsetsBuffer.set(contentInsets);
416            translateRectInAppWindowToScreen(mContentInsetsBuffer);
417            return mContentInsetsBuffer;
418        }
419
420        /**
421         * Translate the visible insets in application window to Screen. This uses
422         * the internal buffer for visible insets to avoid extra object allocation.
423         */
424        public Rect getTranslatedVisibleInsets(Rect visibleInsets) {
425            if (mVisibleInsetsBuffer == null) mVisibleInsetsBuffer = new Rect();
426            mVisibleInsetsBuffer.set(visibleInsets);
427            translateRectInAppWindowToScreen(mVisibleInsetsBuffer);
428            return mVisibleInsetsBuffer;
429        }
430
431        /**
432         * Translate the touchable area in application window to Screen. This uses
433         * the internal buffer for touchable area to avoid extra object allocation.
434         */
435        public Region getTranslatedTouchableArea(Region touchableArea) {
436            if (mTouchableAreaBuffer == null) mTouchableAreaBuffer = new Region();
437            mTouchableAreaBuffer.set(touchableArea);
438            mTouchableAreaBuffer.scale(applicationScale);
439            return mTouchableAreaBuffer;
440        }
441    }
442
443    public void applyToDisplayMetrics(DisplayMetrics inoutDm) {
444        if (!supportsScreen()) {
445            // This is a larger screen device and the app is not
446            // compatible with large screens, so diddle it.
447            CompatibilityInfo.computeCompatibleScaling(inoutDm, inoutDm);
448        } else {
449            inoutDm.widthPixels = inoutDm.noncompatWidthPixels;
450            inoutDm.heightPixels = inoutDm.noncompatHeightPixels;
451        }
452
453        if (isScalingRequired()) {
454            float invertedRatio = applicationInvertedScale;
455            inoutDm.density = inoutDm.noncompatDensity * invertedRatio;
456            inoutDm.densityDpi = (int)((inoutDm.noncompatDensityDpi * invertedRatio) + .5f);
457            inoutDm.scaledDensity = inoutDm.noncompatScaledDensity * invertedRatio;
458            inoutDm.xdpi = inoutDm.noncompatXdpi * invertedRatio;
459            inoutDm.ydpi = inoutDm.noncompatYdpi * invertedRatio;
460            inoutDm.widthPixels = (int) (inoutDm.widthPixels * invertedRatio + 0.5f);
461            inoutDm.heightPixels = (int) (inoutDm.heightPixels * invertedRatio + 0.5f);
462        }
463    }
464
465    public void applyToConfiguration(int displayDensity, Configuration inoutConfig) {
466        if (!supportsScreen()) {
467            // This is a larger screen device and the app is not
468            // compatible with large screens, so we are forcing it to
469            // run as if the screen is normal size.
470            inoutConfig.screenLayout =
471                    (inoutConfig.screenLayout&~Configuration.SCREENLAYOUT_SIZE_MASK)
472                    | Configuration.SCREENLAYOUT_SIZE_NORMAL;
473            inoutConfig.screenWidthDp = inoutConfig.compatScreenWidthDp;
474            inoutConfig.screenHeightDp = inoutConfig.compatScreenHeightDp;
475            inoutConfig.smallestScreenWidthDp = inoutConfig.compatSmallestScreenWidthDp;
476        }
477        inoutConfig.densityDpi = displayDensity;
478        if (isScalingRequired()) {
479            float invertedRatio = applicationInvertedScale;
480            inoutConfig.densityDpi = (int)((inoutConfig.densityDpi * invertedRatio) + .5f);
481        }
482    }
483
484    /**
485     * Compute the frame Rect for applications runs under compatibility mode.
486     *
487     * @param dm the display metrics used to compute the frame size.
488     * @param outDm If non-null the width and height will be set to their scaled values.
489     * @return Returns the scaling factor for the window.
490     */
491    public static float computeCompatibleScaling(DisplayMetrics dm, DisplayMetrics outDm) {
492        final int width = dm.noncompatWidthPixels;
493        final int height = dm.noncompatHeightPixels;
494        int shortSize, longSize;
495        if (width < height) {
496            shortSize = width;
497            longSize = height;
498        } else {
499            shortSize = height;
500            longSize = width;
501        }
502        int newShortSize = (int)(DEFAULT_NORMAL_SHORT_DIMENSION * dm.density + 0.5f);
503        float aspect = ((float)longSize) / shortSize;
504        if (aspect > MAXIMUM_ASPECT_RATIO) {
505            aspect = MAXIMUM_ASPECT_RATIO;
506        }
507        int newLongSize = (int)(newShortSize * aspect + 0.5f);
508        int newWidth, newHeight;
509        if (width < height) {
510            newWidth = newShortSize;
511            newHeight = newLongSize;
512        } else {
513            newWidth = newLongSize;
514            newHeight = newShortSize;
515        }
516
517        float sw = width/(float)newWidth;
518        float sh = height/(float)newHeight;
519        float scale = sw < sh ? sw : sh;
520        if (scale < 1) {
521            scale = 1;
522        }
523
524        if (outDm != null) {
525            outDm.widthPixels = newWidth;
526            outDm.heightPixels = newHeight;
527        }
528
529        return scale;
530    }
531
532    @Override
533    public boolean equals(Object o) {
534        if (this == o) {
535            return true;
536        }
537        try {
538            CompatibilityInfo oc = (CompatibilityInfo)o;
539            if (mCompatibilityFlags != oc.mCompatibilityFlags) return false;
540            if (applicationDensity != oc.applicationDensity) return false;
541            if (applicationScale != oc.applicationScale) return false;
542            if (applicationInvertedScale != oc.applicationInvertedScale) return false;
543            return true;
544        } catch (ClassCastException e) {
545            return false;
546        }
547    }
548
549    @Override
550    public String toString() {
551        StringBuilder sb = new StringBuilder(128);
552        sb.append("{");
553        sb.append(applicationDensity);
554        sb.append("dpi");
555        if (isScalingRequired()) {
556            sb.append(" ");
557            sb.append(applicationScale);
558            sb.append("x");
559        }
560        if (!supportsScreen()) {
561            sb.append(" resizing");
562        }
563        if (neverSupportsScreen()) {
564            sb.append(" never-compat");
565        }
566        if (alwaysSupportsScreen()) {
567            sb.append(" always-compat");
568        }
569        sb.append("}");
570        return sb.toString();
571    }
572
573    @Override
574    public int hashCode() {
575        int result = 17;
576        result = 31 * result + mCompatibilityFlags;
577        result = 31 * result + applicationDensity;
578        result = 31 * result + Float.floatToIntBits(applicationScale);
579        result = 31 * result + Float.floatToIntBits(applicationInvertedScale);
580        return result;
581    }
582
583    @Override
584    public int describeContents() {
585        return 0;
586    }
587
588    @Override
589    public void writeToParcel(Parcel dest, int flags) {
590        dest.writeInt(mCompatibilityFlags);
591        dest.writeInt(applicationDensity);
592        dest.writeFloat(applicationScale);
593        dest.writeFloat(applicationInvertedScale);
594    }
595
596    public static final Parcelable.Creator<CompatibilityInfo> CREATOR
597            = new Parcelable.Creator<CompatibilityInfo>() {
598        @Override
599        public CompatibilityInfo createFromParcel(Parcel source) {
600            return new CompatibilityInfo(source);
601        }
602
603        @Override
604        public CompatibilityInfo[] newArray(int size) {
605            return new CompatibilityInfo[size];
606        }
607    };
608
609    private CompatibilityInfo(Parcel source) {
610        mCompatibilityFlags = source.readInt();
611        applicationDensity = source.readInt();
612        applicationScale = source.readFloat();
613        applicationInvertedScale = source.readFloat();
614    }
615}
616