Utils.java revision f9c17a244f7ac320808b45cf4d9d4bbe8c5343c0
1/*
2 * Copyright (C) 2012 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.deskclock;
18
19import android.animation.Animator;
20import android.animation.AnimatorSet;
21import android.animation.ObjectAnimator;
22import android.animation.TimeInterpolator;
23import android.app.AlarmManager;
24import android.app.PendingIntent;
25import android.content.Context;
26import android.content.Intent;
27import android.content.SharedPreferences;
28import android.content.pm.PackageInfo;
29import android.content.pm.PackageManager.NameNotFoundException;
30import android.content.res.Resources;
31import android.graphics.Color;
32import android.graphics.Paint;
33import android.graphics.PorterDuff;
34import android.graphics.PorterDuffColorFilter;
35import android.net.Uri;
36import android.os.Build;
37import android.os.Handler;
38import android.os.SystemClock;
39import android.preference.PreferenceManager;
40import android.provider.Settings;
41import android.text.Spannable;
42import android.text.SpannableString;
43import android.text.TextUtils;
44import android.text.format.DateFormat;
45import android.text.format.DateUtils;
46import android.text.format.Time;
47import android.text.style.AbsoluteSizeSpan;
48import android.text.style.StyleSpan;
49import android.text.style.TypefaceSpan;
50import android.view.MenuItem;
51import android.view.View;
52import android.view.animation.AccelerateInterpolator;
53import android.view.animation.DecelerateInterpolator;
54import android.widget.TextClock;
55import android.widget.TextView;
56
57import com.android.deskclock.stopwatch.Stopwatches;
58import com.android.deskclock.timer.Timers;
59import com.android.deskclock.worldclock.CityObj;
60
61import java.text.SimpleDateFormat;
62import java.util.Calendar;
63import java.util.Date;
64import java.util.Locale;
65import java.util.TimeZone;
66
67
68public class Utils {
69    private final static String PARAM_LANGUAGE_CODE = "hl";
70
71    /**
72     * Help URL query parameter key for the app version.
73     */
74    private final static String PARAM_VERSION = "version";
75
76    /**
77     * Cached version code to prevent repeated calls to the package manager.
78     */
79    private static String sCachedVersionCode = null;
80
81    /** Types that may be used for clock displays. **/
82    public static final String CLOCK_TYPE_DIGITAL = "digital";
83    public static final String CLOCK_TYPE_ANALOG = "analog";
84
85    /**
86     * Returns whether the SDK is KitKat or later
87     */
88    public static boolean isKitKatOrLater() {
89        return Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2;
90    }
91
92
93    public static void prepareHelpMenuItem(Context context, MenuItem helpMenuItem) {
94        String helpUrlString = context.getResources().getString(R.string.desk_clock_help_url);
95        if (TextUtils.isEmpty(helpUrlString)) {
96            // The help url string is empty or null, so set the help menu item to be invisible.
97            helpMenuItem.setVisible(false);
98            return;
99        }
100        // The help url string exists, so first add in some extra query parameters.  87
101        final Uri fullUri = uriWithAddedParameters(context, Uri.parse(helpUrlString));
102
103        // Then, create an intent that will be fired when the user
104        // selects this help menu item.
105        Intent intent = new Intent(Intent.ACTION_VIEW, fullUri);
106        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
107                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
108
109        // Set the intent to the help menu item, show the help menu item in the overflow
110        // menu, and make it visible.
111        helpMenuItem.setIntent(intent);
112        helpMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
113        helpMenuItem.setVisible(true);
114    }
115
116    /**
117     * Adds two query parameters into the Uri, namely the language code and the version code
118     * of the application's package as gotten via the context.
119     * @return the uri with added query parameters
120     */
121    private static Uri uriWithAddedParameters(Context context, Uri baseUri) {
122        Uri.Builder builder = baseUri.buildUpon();
123
124        // Add in the preferred language
125        builder.appendQueryParameter(PARAM_LANGUAGE_CODE, Locale.getDefault().toString());
126
127        // Add in the package version code
128        if (sCachedVersionCode == null) {
129            // There is no cached version code, so try to get it from the package manager.
130            try {
131                // cache the version code
132                PackageInfo info = context.getPackageManager().getPackageInfo(
133                        context.getPackageName(), 0);
134                sCachedVersionCode = Integer.toString(info.versionCode);
135
136                // append the version code to the uri
137                builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
138            } catch (NameNotFoundException e) {
139                // Cannot find the package name, so don't add in the version parameter
140                // This shouldn't happen.
141                Log.wtf("Invalid package name for context " + e);
142            }
143        } else {
144            builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
145        }
146
147        // Build the full uri and return it
148        return builder.build();
149    }
150
151    public static long getTimeNow() {
152        return SystemClock.elapsedRealtime();
153    }
154
155    /**
156     * Calculate the amount by which the radius of a CircleTimerView should be offset by the any
157     * of the extra painted objects.
158     */
159    public static float calculateRadiusOffset(
160            float strokeSize, float dotStrokeSize, float markerStrokeSize) {
161        return Math.max(strokeSize, Math.max(dotStrokeSize, markerStrokeSize));
162    }
163
164    /**
165     * Uses {@link Utils#calculateRadiusOffset(float, float, float)} after fetching the values
166     * from the resources just as {@link CircleTimerView#init(android.content.Context)} does.
167     */
168    public static float calculateRadiusOffset(Resources resources) {
169        if (resources != null) {
170            float strokeSize = resources.getDimension(R.dimen.circletimer_circle_size);
171            float dotStrokeSize = resources.getDimension(R.dimen.circletimer_dot_size);
172            float markerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size);
173            return calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize);
174        } else {
175            return 0f;
176        }
177    }
178
179    /**  The pressed color used throughout the app. If this method is changed, it will not have
180     *   any effect on the button press states, and those must be changed separately.
181    **/
182    public static int getPressedColorId() {
183        return R.color.clock_red;
184    }
185
186    /**  The un-pressed color used throughout the app. If this method is changed, it will not have
187     *   any effect on the button press states, and those must be changed separately.
188    **/
189    public static int getGrayColorId() {
190        return R.color.clock_gray;
191    }
192
193    /**
194     * Clears the persistent data of stopwatch (start time, state, laps, etc...).
195     */
196    public static void clearSwSharedPref(SharedPreferences prefs) {
197        SharedPreferences.Editor editor = prefs.edit();
198        editor.remove (Stopwatches.PREF_START_TIME);
199        editor.remove (Stopwatches.PREF_ACCUM_TIME);
200        editor.remove (Stopwatches.PREF_STATE);
201        int lapNum = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET);
202        for (int i = 0; i < lapNum; i++) {
203            String key = Stopwatches.PREF_LAP_TIME + Integer.toString(i);
204            editor.remove(key);
205        }
206        editor.remove(Stopwatches.PREF_LAP_NUM);
207        editor.apply();
208    }
209
210    /**
211     * Broadcast a message to show the in-use timers in the notifications
212     */
213    public static void showInUseNotifications(Context context) {
214        Intent timerIntent = new Intent();
215        timerIntent.setAction(Timers.NOTIF_IN_USE_SHOW);
216        context.sendBroadcast(timerIntent);
217    }
218
219    /**
220     * Broadcast a message to show the in-use timers in the notifications
221     */
222    public static void showTimesUpNotifications(Context context) {
223        Intent timerIntent = new Intent();
224        timerIntent.setAction(Timers.NOTIF_TIMES_UP_SHOW);
225        context.sendBroadcast(timerIntent);
226    }
227
228    /**
229     * Broadcast a message to cancel the in-use timers in the notifications
230     */
231    public static void cancelTimesUpNotifications(Context context) {
232        Intent timerIntent = new Intent();
233        timerIntent.setAction(Timers.NOTIF_TIMES_UP_CANCEL);
234        context.sendBroadcast(timerIntent);
235    }
236
237    /** Runnable for use with screensaver and dream, to move the clock every minute.
238     *  registerViews() must be called prior to posting.
239     */
240    public static class ScreensaverMoveSaverRunnable implements Runnable {
241        static final long MOVE_DELAY = 60000; // DeskClock.SCREEN_SAVER_MOVE_DELAY;
242        static final long SLIDE_TIME = 10000;
243        static final long FADE_TIME = 3000;
244
245        static final boolean SLIDE = false;
246
247        private View mContentView, mSaverView;
248        private final Handler mHandler;
249
250        private static TimeInterpolator mSlowStartWithBrakes;
251
252
253        public ScreensaverMoveSaverRunnable(Handler handler) {
254            mHandler = handler;
255            mSlowStartWithBrakes = new TimeInterpolator() {
256                @Override
257                public float getInterpolation(float x) {
258                    return (float)(Math.cos((Math.pow(x,3) + 1) * Math.PI) / 2.0f) + 0.5f;
259                }
260            };
261        }
262
263        public void registerViews(View contentView, View saverView) {
264            mContentView = contentView;
265            mSaverView = saverView;
266        }
267
268        @Override
269        public void run() {
270            long delay = MOVE_DELAY;
271            if (mContentView == null || mSaverView == null) {
272                mHandler.removeCallbacks(this);
273                mHandler.postDelayed(this, delay);
274                return;
275            }
276
277            final float xrange = mContentView.getWidth() - mSaverView.getWidth();
278            final float yrange = mContentView.getHeight() - mSaverView.getHeight();
279
280            if (xrange == 0 && yrange == 0) {
281                delay = 500; // back in a split second
282            } else {
283                final int nextx = (int) (Math.random() * xrange);
284                final int nexty = (int) (Math.random() * yrange);
285
286                if (mSaverView.getAlpha() == 0f) {
287                    // jump right there
288                    mSaverView.setX(nextx);
289                    mSaverView.setY(nexty);
290                    ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f)
291                        .setDuration(FADE_TIME)
292                        .start();
293                } else {
294                    AnimatorSet s = new AnimatorSet();
295                    Animator xMove   = ObjectAnimator.ofFloat(mSaverView,
296                                         "x", mSaverView.getX(), nextx);
297                    Animator yMove   = ObjectAnimator.ofFloat(mSaverView,
298                                         "y", mSaverView.getY(), nexty);
299
300                    Animator xShrink = ObjectAnimator.ofFloat(mSaverView, "scaleX", 1f, 0.85f);
301                    Animator xGrow   = ObjectAnimator.ofFloat(mSaverView, "scaleX", 0.85f, 1f);
302
303                    Animator yShrink = ObjectAnimator.ofFloat(mSaverView, "scaleY", 1f, 0.85f);
304                    Animator yGrow   = ObjectAnimator.ofFloat(mSaverView, "scaleY", 0.85f, 1f);
305                    AnimatorSet shrink = new AnimatorSet(); shrink.play(xShrink).with(yShrink);
306                    AnimatorSet grow = new AnimatorSet(); grow.play(xGrow).with(yGrow);
307
308                    Animator fadeout = ObjectAnimator.ofFloat(mSaverView, "alpha", 1f, 0f);
309                    Animator fadein = ObjectAnimator.ofFloat(mSaverView, "alpha", 0f, 1f);
310
311
312                    if (SLIDE) {
313                        s.play(xMove).with(yMove);
314                        s.setDuration(SLIDE_TIME);
315
316                        s.play(shrink.setDuration(SLIDE_TIME/2));
317                        s.play(grow.setDuration(SLIDE_TIME/2)).after(shrink);
318                        s.setInterpolator(mSlowStartWithBrakes);
319                    } else {
320                        AccelerateInterpolator accel = new AccelerateInterpolator();
321                        DecelerateInterpolator decel = new DecelerateInterpolator();
322
323                        shrink.setDuration(FADE_TIME).setInterpolator(accel);
324                        fadeout.setDuration(FADE_TIME).setInterpolator(accel);
325                        grow.setDuration(FADE_TIME).setInterpolator(decel);
326                        fadein.setDuration(FADE_TIME).setInterpolator(decel);
327                        s.play(shrink);
328                        s.play(fadeout);
329                        s.play(xMove.setDuration(0)).after(FADE_TIME);
330                        s.play(yMove.setDuration(0)).after(FADE_TIME);
331                        s.play(fadein).after(FADE_TIME);
332                        s.play(grow).after(FADE_TIME);
333                    }
334                    s.start();
335                }
336
337                long now = System.currentTimeMillis();
338                long adjust = (now % 60000);
339                delay = delay
340                        + (MOVE_DELAY - adjust) // minute aligned
341                        - (SLIDE ? 0 : FADE_TIME) // start moving before the fade
342                        ;
343            }
344
345            mHandler.removeCallbacks(this);
346            mHandler.postDelayed(this, delay);
347        }
348    }
349
350    /** Setup to find out when the quarter-hour changes (e.g. Kathmandu is GMT+5:45) **/
351    public static long getAlarmOnQuarterHour() {
352        Calendar nextQuarter = Calendar.getInstance();
353        //  Set 1 second to ensure quarter-hour threshold passed.
354        nextQuarter.set(Calendar.SECOND, 1);
355        int minute = nextQuarter.get(Calendar.MINUTE);
356        nextQuarter.add(Calendar.MINUTE, 15 - (minute % 15));
357        long alarmOnQuarterHour = nextQuarter.getTimeInMillis();
358        if (0 >= (alarmOnQuarterHour - System.currentTimeMillis())
359                || (alarmOnQuarterHour - System.currentTimeMillis()) > 901000) {
360            Log.wtf("quarterly alarm calculation error");
361        }
362        Log.v("getAlarmOnQuarterHour returns " // STOPSHIP Don't ship with this log
363                + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(nextQuarter.getTime())
364                + " to fire in " + (alarmOnQuarterHour - System.currentTimeMillis()));
365        return alarmOnQuarterHour;
366    }
367
368    // Setup a thread that starts at midnight plus one second. The extra second is added to ensure
369    // the date has changed.
370    public static void setMidnightUpdater(Handler handler, Runnable runnable) {
371        String timezone = TimeZone.getDefault().getID();
372        if (handler == null || runnable == null || timezone == null) {
373            return;
374        }
375        long now = System.currentTimeMillis();
376        Time time = new Time(timezone);
377        time.set(now);
378        long runInMillis = ((24 - time.hour) * 3600 - time.minute * 60 - time.second + 1) * 1000;
379        handler.removeCallbacks(runnable);
380        handler.postDelayed(runnable, runInMillis);
381    }
382
383    // Stop the midnight update thread
384    public static void cancelMidnightUpdater(Handler handler, Runnable runnable) {
385        if (handler == null || runnable == null) {
386            return;
387        }
388        handler.removeCallbacks(runnable);
389    }
390
391    // Setup a thread that starts at the quarter-hour plus one second. The extra second is added to
392    // ensure dates have changed.
393    public static void setQuarterHourUpdater(Handler handler, Runnable runnable) {
394        String timezone = TimeZone.getDefault().getID();
395        if (handler == null || runnable == null || timezone == null) {
396            return;
397        }
398        long runInMillis = getAlarmOnQuarterHour() - System.currentTimeMillis();
399        // Ensure the delay is at least one second.
400        if (runInMillis < 1000) {
401            runInMillis = 1000;
402        }
403        handler.removeCallbacks(runnable);
404        handler.postDelayed(runnable, runInMillis);
405    }
406
407    // Stop the quarter-hour update thread
408    public static void cancelQuarterHourUpdater(Handler handler, Runnable runnable) {
409        if (handler == null || runnable == null) {
410            return;
411        }
412        handler.removeCallbacks(runnable);
413    }
414
415    /**
416     * For screensavers to set whether the digital or analog clock should be displayed.
417     * Returns the view to be displayed.
418     */
419    public static View setClockStyle(Context context, View digitalClock, View analogClock,
420            String clockStyleKey) {
421        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
422        String defaultClockStyle = context.getResources().getString(R.string.default_clock_style);
423        String style = sharedPref.getString(clockStyleKey, defaultClockStyle);
424        View returnView;
425        if (style.equals(CLOCK_TYPE_ANALOG)) {
426            digitalClock.setVisibility(View.GONE);
427            analogClock.setVisibility(View.VISIBLE);
428            returnView = analogClock;
429        } else {
430            digitalClock.setVisibility(View.VISIBLE);
431            analogClock.setVisibility(View.GONE);
432            returnView = digitalClock;
433        }
434
435        return returnView;
436    }
437
438    /**
439     * For screensavers to dim the lights if necessary.
440     */
441    public static void dimClockView(boolean dim, View clockView) {
442        Paint paint = new Paint();
443        paint.setColor(Color.WHITE);
444        paint.setColorFilter(new PorterDuffColorFilter(
445                        (dim ? 0x40FFFFFF : 0xC0FFFFFF),
446                PorterDuff.Mode.MULTIPLY));
447        clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint);
448    }
449
450    /** Clock views can call this to refresh their alarm to the next upcoming value. **/
451    public static void refreshAlarm(Context context, View clock) {
452        String nextAlarm = Settings.System.getString(context.getContentResolver(),
453                Settings.System.NEXT_ALARM_FORMATTED);
454        TextView nextAlarmView;
455        nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm);
456        if (!TextUtils.isEmpty(nextAlarm) && nextAlarmView != null) {
457            nextAlarmView.setText(
458                    context.getString(R.string.control_set_alarm_with_existing, nextAlarm));
459            nextAlarmView.setContentDescription(context.getResources().getString(
460                    R.string.next_alarm_description, nextAlarm));
461            nextAlarmView.setVisibility(View.VISIBLE);
462        } else  {
463            nextAlarmView.setVisibility(View.GONE);
464        }
465    }
466
467    /** Clock views can call this to refresh their date. **/
468    public static void updateDate(
469            String dateFormat, String dateFormatForAccessibility, View clock) {
470
471        Date now = new Date();
472        TextView dateDisplay;
473        dateDisplay = (TextView) clock.findViewById(R.id.date);
474        if (dateDisplay != null) {
475            final Locale l = Locale.getDefault();
476            String fmt = DateFormat.getBestDateTimePattern(l, dateFormat);
477            SimpleDateFormat sdf = new SimpleDateFormat(fmt, l);
478            dateDisplay.setText(sdf.format(now));
479            dateDisplay.setVisibility(View.VISIBLE);
480            fmt = DateFormat.getBestDateTimePattern(l, dateFormatForAccessibility);
481            sdf = new SimpleDateFormat(fmt, l);
482            dateDisplay.setContentDescription(sdf.format(now));
483        }
484    }
485
486    /***
487     * Formats the time in the TextClock according to the Locale with a special
488     * formatting treatment for the am/pm label.
489     * @param clock - TextClock to format
490     * @param amPmFontSize - size of the am/pm label since it is usually smaller
491     *        than the clock time size.
492     */
493    public static void setTimeFormat(TextClock clock, int amPmFontSize) {
494        if (clock != null) {
495            // Get the best format for 12 hours mode according to the locale
496            clock.setFormat12Hour(get12ModeFormat(amPmFontSize));
497            // Get the best format for 24 hours mode according to the locale
498            clock.setFormat24Hour(get24ModeFormat());
499        }
500    }
501    /***
502     * @param amPmFontSize - size of am/pm label (label removed is size is 0).
503     * @return format string for 12 hours mode time
504     */
505    public static CharSequence get12ModeFormat(int amPmFontSize) {
506        String skeleton = (amPmFontSize > 0) ? "hma" : "hm";
507        String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
508        // Replace spaces with "Hair Space"
509        pattern = pattern.replaceAll(" ", "\u200A");
510        // Build a spannable so that the am/pm will be formatted
511        int amPmPos = pattern.indexOf('a');
512        if (amPmPos == -1) {
513            return pattern;
514        }
515        Spannable sp = new SpannableString(pattern);
516        sp.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), amPmPos, amPmPos + 1,
517                Spannable.SPAN_POINT_MARK);
518        sp.setSpan(new AbsoluteSizeSpan(amPmFontSize), amPmPos, amPmPos + 1,
519                Spannable.SPAN_POINT_MARK);
520        sp.setSpan(new TypefaceSpan("sans-serif-condensed"), amPmPos, amPmPos + 1,
521                Spannable.SPAN_POINT_MARK);
522        return sp;
523    }
524
525    public static CharSequence get24ModeFormat() {
526        String skeleton = "Hm";
527        return DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
528    }
529
530    public static CityObj[] loadCitiesFromXml(Context c) {
531        Resources r = c.getResources();
532        // Read strings array of name,timezone, id
533        // make sure the list are the same length
534        String[] cities = r.getStringArray(R.array.cities_names);
535        String[] timezones = r.getStringArray(R.array.cities_tz);
536        String[] ids = r.getStringArray(R.array.cities_id);
537        int minLength = cities.length;
538        if (cities.length != timezones.length || ids.length != cities.length) {
539            // StopShip: Make sure to remove this after we get transations for K
540            minLength = Math.min(cities.length, Math.min(timezones.length, ids.length));
541            Log.e("City lists sizes are not the same, trancating");
542            // return null;
543        }
544        CityObj[] tempList = new CityObj[minLength];
545        for (int i = 0; i < cities.length; i++) {
546            tempList[i] = new CityObj(cities[i], timezones[i], ids[i]);
547        }
548        return tempList;
549    }
550
551    /**
552     * Returns string denoting the timezone hour offset (e.g. GMT-8:00)
553     */
554    public static String getGMTHourOffset(TimeZone timezone, boolean showMinutes) {
555        StringBuilder sb = new StringBuilder();
556        sb.append("GMT");
557        int gmtOffset = timezone.getRawOffset();
558        if (gmtOffset < 0) {
559            sb.append('-');
560        } else {
561            sb.append('+');
562        }
563        sb.append(Math.abs(gmtOffset) / DateUtils.HOUR_IN_MILLIS); // Hour
564
565        if (showMinutes) {
566            final int min = (Math.abs(gmtOffset) / (int) DateUtils.MINUTE_IN_MILLIS) % 60;
567            sb.append(':');
568            if (min < 10) {
569                sb.append('0');
570            }
571            sb.append(min);
572        }
573
574        return sb.toString();
575    }
576
577    public static String getCityName(CityObj city, CityObj dbCity) {
578        return (city.mCityId == null || dbCity == null) ? city.mCityName : dbCity.mCityName;
579    }
580}
581