воскресенье, 21 ноября 2010 г.

Andrid widget, part 0.2

Бесполезная часть почти закончилась,дальше будет полезная; полезная – в том смысле, что виджет будет делать хоть что-то, для чего он придуман, показывать время

Но сначала отступление лирическое: поскольку я не прочитал ни одной книги про яву, я не очень представляю архитектуру программы в андроиде. Поэтому я тут придумал велосипед, связанный со следующим: мне нужен код, который обновляет значением текущего времени виджет, нужен в 2-х разных местах – сразу после настройки виджета, после добавления его на экран, и где-то, где будет учитываться ход времени.

Логично, если этот код будет не дублироваться в 2-х разных частях программы, логично сделать скажем класс, который будет обновлять виджет (и мало ли что еще); и вот тут я не понимаю следующего: понятно, что в классе, реализующем настройки виджета, я создаю обьект этого воображаемого пока класса и вызываю метод “ОбновитьВнешнийВид”. В другом месте программы наступает событие “время изменилось”, мне надо что – опять создать обьект и вызвать у него нужный метод? Т.е. я не очень представляю себе, как “расшарить” обьект между разными классами – потому что я уже знаю, что в ява-программах нет глобальных переменных.

Поэтому я сделал так: я сделал класс, который хранит текущее значение для часов, минут, даты и месяца, у которого есть метод для обновления значений, и есть статический метод, возвращающий экземпляр класса. Я могу путаться в терминах, поэтому на пальцах: под статическим методом я имею в виду метод класса, т.е. метод, который можно вызывать не у экземпляра класса, а у самого класса. В этом методе я проверяю – есть ли уже готовый обьект и если есть, то возвращаю его; а этот обьект тоже обьявлен как статический, но наск. я понял смысл в данном случае в том, что будучи созданным в этом методе этот обьект не уничтожится при выходе из метода

В общем, продолжаем марлезонский балет: создаем класс, который будет обновлять цифирки/подписи в виджете:

package info.hamster.simpleclock.widget;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.format.DateUtils;
import android.util.Log;
import android.widget.RemoteViews;

import android.content.Context;
import java.util.Calendar;

public class DataRefresher {

    final static String TAG = "Data Refresher Class:";

    private int curHour, curMin, curDate, curDay, curMonth;
    Context ownerContext = null;

    final static int res_map[] = {
            R.drawable.d0, R.drawable.d1, R.drawable.d2, R.drawable.d3, R.drawable.d4,
            R.drawable.d5, R.drawable.d6, R.drawable.d7, R.drawable.d8, R.drawable.d9
    };
    // мне надо соответствие - какой ресурс использовать для цифры скажем 4?
    // а вот какой - значение 4-го элемента массива - а там id цифры "4"

    static DataRefresher data = null;
    // поскольку код работает, то: это расшариваемый экземпляр класса,
    // который возвращается следующей функцией желающим его использовать

    static public DataRefresher getObject (Context context) {

        if (data == null)
        {
            data = new DataRefresher();
            data.ownerContext = context;
        }

        return data;
    }

    // все, что касается действий с виджетом, я выношу сюда - в т.ч. и 
    // присваивание действий тапам по дате/времени
    // код просто переехал из одного класса в другой, заодно добавив поведения
    // если в настройках не указано, какая программа вызывается по тапу - 
    // вызываем настройку нашего виджета
    public void UpdateActions () {

        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ownerContext);
        RemoteViews remoteViews = new RemoteViews(ownerContext.getPackageName(), R.layout.main);

        String actionPackage = prefs.getString("clockAction", "");
        Intent intent = null;
        if (actionPackage.equals("")) {
            intent = new Intent(ownerContext, ConfigClass.class);
        } else {
            intent = new Intent(Intent.ACTION_MAIN);
            ComponentName name = new ComponentName(actionPackage, prefs.getString("clockActivity", ""));

            intent.addCategory(Intent.CATEGORY_LAUNCHER);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                    Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
            intent.setComponent(name);
        }
        // создаем активити, которое вызовется не прямо сейчас, а когда-то потом -
        // в данном случае тогда, когда тапнем по лайоуту с id = btnTime - т.е.
        // в котором цифры времени располагаются
        PendingIntent pendingIntent = PendingIntent.getActivity(ownerContext, 0, intent, 0);
        remoteViews.setOnClickPendingIntent(R.id.btnTime, pendingIntent);

        actionPackage = prefs.getString("dateAction", "");
        if (actionPackage.equals("")) {
            intent = new Intent(ownerContext, ConfigClass.class);
        } else {
            intent = new Intent(Intent.ACTION_MAIN);
            ComponentName name = new ComponentName(actionPackage, prefs.getString("dateActivity", ""));

            intent.addCategory(Intent.CATEGORY_LAUNCHER);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                    Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
            intent.setComponent(name);
        }
        pendingIntent = PendingIntent.getActivity(ownerContext, 0, intent, 0);
        remoteViews.setOnClickPendingIntent(R.id.btnDate, pendingIntent);

        ComponentName thisWidget = new ComponentName(ownerContext, SimpleClockClass.class);
        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(ownerContext);

        appWidgetManager.updateAppWidget(thisWidget, remoteViews);
        Log.d(TAG, "actions updated.");
    }

    // этот метод обновляет виджет - т.е. выводит в него правильное время/дату
    public void UpdateData(boolean invalid) {

        Calendar CurDate = Calendar.getInstance();

        // смысл тут в том, что можно принудительно обновить виджет значениями, а
        // можно обновить только если время поменялсь на минуту
        // вообще это осталось еще с варианта, когда таймер проверял 
        // время каждую секунду 
        if (CurDate.get(Calendar.HOUR_OF_DAY) != curHour) {
            curHour = CurDate.get(Calendar.HOUR_OF_DAY);
            invalid = true;
        }

        if (CurDate.get(Calendar.MINUTE) != curMin) {
            curMin = CurDate.get(Calendar.MINUTE);
            invalid = true;
        }

        if (CurDate.get(Calendar.DATE) != curDate) {
            curDate = CurDate.get(Calendar.DATE);
            curDay = CurDate.get(Calendar.DAY_OF_WEEK);
            curMonth = CurDate.get(Calendar.MONTH);
            invalid = true;
        }

        if (invalid) {
            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ownerContext);
            RemoteViews remoteViews = new RemoteViews(ownerContext.getPackageName(), R.layout.main);
            ComponentName thisWidget = new ComponentName(ownerContext, SimpleClockClass.class);
            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(ownerContext);


            int h = curHour;
            // реализуем настройку "12/24 отображение"
            if ((!prefs.getBoolean("timeFormat", true)) && (curHour > 12))
                h -= 12;

            // обновили цифры
            remoteViews.setImageViewResource(R.id.hour1, res_map[h / 10]);
            remoteViews.setImageViewResource(R.id.hour2, res_map[h % 10]);
            remoteViews.setImageViewResource(R.id.min1, res_map[curMin / 10]);
            remoteViews.setImageViewResource(R.id.min2, res_map[curMin % 10]);
            remoteViews.setImageViewResource(R.id.date1, res_map[curDate / 10]);
            remoteViews.setImageViewResource(R.id.date2, res_map[curDate % 10]);

            // обновили день недели и месяц
            remoteViews.setTextViewText(R.id.cur_day, DateUtils.getDayOfWeekString(curDay, DateUtils.LENGTH_LONG));
            int x = curDay;
            String temp = "";
            // любимый прием мой - "тут тупо": тут тупо в один контрол выводится
            // значение текущего месяца или дня недели, а во второй - строка 
            // с остальными днями/месяцами (даже если они там точно 
            // уже не поместятся)
            do {
                if (++x > Calendar.SATURDAY)
                    x = Calendar.SUNDAY;

                temp = temp + DateUtils.getDayOfWeekString(x, DateUtils.LENGTH_LONG) + " ";
            } while (x != curDay);
            remoteViews.setTextViewText(R.id.days, " " + temp);

            remoteViews.setTextViewText(R.id.cur_month, DateUtils.getMonthString(curMonth, DateUtils.LENGTH_LONG));
            x = curMonth;
            temp = "";
            do {
                if (++x > Calendar.DECEMBER)
                    x = Calendar.JANUARY;
                temp = temp + DateUtils.getMonthString(x, DateUtils.LENGTH_LONG) + " ";
            } while (x != curMonth);
            remoteViews.setTextViewText(R.id.months, " " + temp);

            appWidgetManager.updateAppWidget(thisWidget, remoteViews);
        }
    }
}

Этот класс будет использоваться после настройки виджета (скажем, если изменился формат вывода времени, надо виджет обновить.. или мы можем поменять программу, которая запускается по тапу на виджете – то же самое, нужно его обновить..

Ну и конечно обновлять его будем при изменении времени; первые мои варианты использовали таймер, срабатывавший раз в секунду, я проверял, поменялось ли значение минут и если да, то обновлял показания. Делалось это для того, чтобы данные виджета менялись синхронно с часами телефона, если они видны. Все работало, но за ночь телефон разряжался полностью и выключался, поэтому в последнем варианте используется подписка на широковещательное событие ACTION_TIME_TICK, которое возникает как раз раз в минуту – значит нужен класс, который будет принимать это событие; в свою очередь этот класс будет создаваться в сервисе, при его старте, а сервис нужен, чтобы андроид не решил, что процесс слишком долго ничего не делает и не убил его, этот самый процесс.

Так что создаем приемник:

package info.hamster.simpleclock.widget;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class TimeEventsReceiver extends BroadcastReceiver {

    final static String TAG = "TimeEventsReceiver:";

    public void onReceive(Context context, Intent intent) {

        DataRefresher refresher = DataRefresher.getObject(null);
        refresher.UpdateData(false);

    }
}

Кода практически нет – только обработчик события, в котором получаем наш обьект, умеющий обновлять виджет, и обновляем его, этот виджет. В манифест добавляется строка, описывающая приемник:

    <receiver android:name=".TimeEventsReceiver" android:label="Time Events Receiver"/>

Создаем сервис, перекрываем методы onCreate и onDestroy, в которых подписываемся или отписываемся от рассылки на ACTION_TIME_TICK:

package info.hamster.simpleclock.widget;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.util.Log;

public class SimpleClockService extends Service {

    static final String TAG = "Simple Clock Service:";

    private BroadcastReceiver receiver;

    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {

        super.onCreate();

        receiver = new TimeEventsReceiver();
        Intent intent = registerReceiver(receiver, new IntentFilter(Intent.ACTION_TIME_TICK));

        DataRefresher refresher = DataRefresher.getObject(getApplicationContext());
        refresher.UpdateData(true);

    }

    @Override
    public void onDestroy() {

        if (receiver != null)
            unregisterReceiver(receiver);
        receiver = null;

        super.onDestroy();
    }
}

Сервис тоже указывается в манифесте:

    <service android:name=".SimpleClockService" android:label="Simple Clock Service"/>

Ну и в классе виджета добавляем код для остановки сервиса (чтобы он не висел после удаления виджета с рабочего стола); сервис стартует не здесь – ведь задумано поведение – после добавления виджета показать экран настройки и только если выбрать из меню “Сохранить настройки” – можно запускать сервис:

package info.hamster.simpleclock.widget;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class SimpleClockClass extends AppWidgetProvider {
    final static String TAG = "Simple Clock Class:";

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        Log.d(TAG, "onUpdate fired..");

    }

    @Override
    public void onDisabled(Context context) {
        context.stopService(new Intent(context, SimpleClockService.class));
        super.onDisabled(context);
    }

    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        context.stopService(new Intent(context, SimpleClockService.class));
        super.onDeleted(context, appWidgetIds);
    }
}

В классе настроек (ConfigClass.java) в обработчике меню меняем:

            case R.id.save_settings:
                Intent resultValue = new Intent();
                resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
                setResult(RESULT_OK, resultValue);

                DataRefresher refresher = DataRefresher.getObject(getApplicationContext());
                refresher.UpdateData(true);
                refresher.UpdateActions();

                Context context = getApplicationContext();
                context.startService(new Intent(context, SimpleClockService.class));


                finish();
                return true;
...

Собственно как будто бы все; у меня в телефоне текущая версия тестится с.. прошлой части, проблем вроде нет, ну кроме недодумок разных, типа: если я вызываю настройку виджета, меняю параметр, но выхожу из настроек без сохранения – настройка не применяется (виджет не обновляется), но в окне настроек запоминается автоматически.

А, да, окно настроек; поскольку окно настроек – активити как активити, то можно сделать финт:если в манифест в описание активити настроек добавить

                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>

то 1) в списке программ появится иконка с подписью Simple Clock Widget Configurator, 2) после ее выбора будет открываться окно настроек.

Еще непонятность с следующим: размеры виджета вроде сделаны по доке, на практике они такие, как и задумано, но – ощущение, что можно сделать его больше: вот как выглядит виджет:

w8

это LauncherPro после перемещения виджета – он (Launcher) может ресайзить виджеты, но тут он “как есть”, в эту область не могут залезть другие виджеты –> ее можно использовать?.. Непонятно.. может это “фича” этого лаунчера..

Проект в текущем состоянии - здесь

2 комментария:

  1. Ссылка с исходниками не работает!!

    ОтветитьУдалить
  2. на fileden (там архив) сейчас висит:

    Maintenance update

    We appreciate your continued patience. As many are aware - we have been making large strides over the past months to increase the speed and efficiency of the Fileden system while simultaneously working to migrate it over to the new platform. However, this process provided to be more challenging and thus taking more time than we initially anticipated.

    Beginning Thursday December 1, 2011 we will begin a phased approached to incrementally providing access to account data. Though the entire process will take approximately 4-7 days to complete all phases, a vast majority will have access in the next 24-48 hours.

    может то у них временно?.. у меня нет просто копии этого архива, мне его хранить в общем ведь смысла нету никакого

    ОтветитьУдалить