Showing posts with label android. Show all posts
Showing posts with label android. Show all posts

Tuesday, March 8, 2011

Ошибка транзакций в приложениях

Мутантская ошибка, которая может послужить причиной длительного брейнфакерства.

Приложение для Андроид (Froyo, но также воспроизводится и на Gingerbread, т.е. встречается на API версий 8-9), по событию читает страницу в интернете и отображает ее в виджете. Вроде вся магия простая как в мультфильмах, но почему-то картинки в виджете в одних случаях показываются, в других - не показываются. Стали разбираться.

Проблемы начинаются после перехода на девайсы с большим разрешением экрана - это стало понятно сразу. Лог - чистый и красивый, за исключением одной строки:
JavaBinder "!!! Failed binder transaction !!!"

Ясно, что кто-то куда-то не может прицепиться, из-за чего система выкидывает ошибку и перестает апдейтить процесс. Но что куда не может прицепиться и по какой причине - оставалось загадкой.

Указанную выше ошибку выкидывает JavaBinder, у которого платформой ограничены размеры транзакций (что-то около 100 кб). Ошибка возникает, если между двумя отдельно взятыми процессами единовременно передается большой (т.е. гораздо больше размера транзакции) кусок информации. В нашем случае, колдунство предполагает создание скриншота веб-страницы в одном процессе и передача его для обновления в виджет. То есть прямо по учебнику.

Ключем к пониманию этой магии послужила смена разрешения экранов. Для того, чтобы сохранить отображаемую веб-страницу в качестве картинки, нам нужно воспользоваться контроллом WebView (переопределив его PictureListener). Получаемая на выходе картинка представляет собой загруженную и отображеную в контролле веб-страницу. Линейные размеры картинки не отличаются в зависимости от количества пукселей на дисплее девайса, однако при отображении в WebView или в виджете размер конечного файла может сильно отличаться.

Например, нам надо отобразить скриншот веб-страницы размером 600х600 пукселей на экране разрешением 300х300 (таких экранов на рынок не выпускают, это для примера). Очевидно, что в этом случае требуется вмешательство волшебства, уменьшающего реальный размер картинки в четыре раза (каждую сторону - в два раза). Понятно, что для экранов разрешением 600х600 уменьшать ничего не придется и размер картинки будет в 4 раза большим. Поэтому для относительно небольших экранов картинки удачно обрабатываются транзакциями, в то время как для больших экранов переброска объемных данных вызывает ошибки в транзакциях: виртуальная машина выбрасывает исключение и перестает обновлять процесс, от этого виджет хоть и существует в памяти, на самом деле не обновляется (в нашем случае он оставался невидимым, но это зависит только от лэйаута).

Самый простой способ предотвратить слишком большой размер файла - принудительное уменьшение размера файла:

public static Bitmap makeBitmap() {
    Bitmap bitmap = null;
    BitmapFactory.Options bmOp = new BitmapFactory.Options();
    String filePath = "foo/bar/baz";

    // Здесь указываем, во сколько раз нужно
    // уменьшить каждую сторону картинки
    bmOp.inSampleSize = 2;
    bitmap = BitmapFactory.decodeFile(filePath, bmOp);

    return bitmap;
}

NB. Рекомендую использовать четные числа для семплирования (в теоретическом разделе интернета всем всегда рады).

Однако такой колдунство не всегда прокатывает по двум причинам:
1. Не всегда понятно, во сколько раз нужно уменьшать размеры
2. Уменьшенная картинка - это уменьшенная картинка, т.е. если вы (или ваш заказчик в лице Докомо) придумали показывать такую картинку на весь экран девайса - картика будет в 2 (3, 4, 5 и т.д.) раз меньше размера экрана.

Может, причин сущетсвует еще больше (практически уверен).

Поэтому сейчас колдунство следует совместить с алхимией. Для этого нужно полученную картинку сначала явно уменьшить до какого-нибудь приемлимого значения, а потом вписать уменьшенную картинку в нужную область на Canvas'е:

public static void makeBitmap(Picture picture) {
    Context con = getApplicationContext();
    Resources r = con.getResources();
    DisplayMetrics me = r.getDisplayMetrics();

    float screenWidth = me.widthPixels;
    float screenHeight = me.heightPixels;

    float screenScale;
    Bitmap b;
    Canvas c;

    int newPicWidth, newPicHeight;

    // Смотрим, во сколько ширина дисплея больше высоты
    screenScale = screenWidth / screenHeight;
    
    // Устанавливаем размер картинки на 80% от размера экрана
    // При разрешении 854х600 ошибок переполнения буффера транзакции
    // возникать не должно, но если что - число можно подрегулировать
    newPicWidth = (int)(screenWidth * 0.8);
    newPicHeight = (int)(newPicWidth / screenScale * 0.8);
    
    // Создаем битмап и холст для него
    b = Bitmap.createBitmap(newPicWidth, newPicHeight, Bitmap.Config.RGB_565);
    c = new Canvas(b);

    Rect dst = new Rect();
    dst.top = 0;
    dst.left = 0;
    dst.right = newPicWidth;
    dst.bottom = picHeight;

    // Рисуем картинку на битмап
    // Картинка растягивается или ужимается при необходимости
    c.drawPicture(picture, dst);
    
    // После этого можем сохранить битмап как файл
    // или открыть его в виджете
}

Sunday, January 30, 2011

Программирование сокетов в Linux

При написании приложения для Андроид иногда (очень редко) возникает необходимость пообщаться с сервером на уровне более низком, чем Java SDK. Например, если вы создаете низкоуровневый сервис, работающий напрямую с данными оператора (загрузка обновления системы, активация телефонного девайса в сети оператора и т.д.), приходится иметь дело с сокетами (в грубом приближении - инетрфейсами между железом и програмной частью).

Работать с сокетами на низком уронде в Андроиде (т.е. на ядре линукс) можно с помощью следующего набора заклинаний.

#define WIN32

// заголовки стандартного C
#include <string.h>
#include <stdio.h>

// подключаем заголовки для разных OS
#if defined WIN32
#include <winsock.h>  // WinSock
#elif defined LINUX
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#endif

// переопределяем некоторые типы и константы
// в зависимости от OS
#if defined WIN32
typedef int socklen_t;  // размер сокета в Unix
#elif defined LINUX
typedef int SOCKET;
#define INVALID_SOCKET -1  // костанта WinSock
#define SOCKET_ERROR   -1  // стандартная ошибка WinSock

// WinSock, в отличие от Unix, не использует файл-дескрипторы
// поэтому переопределяем наименование метода
#define closesocket(s) close(s);  
#endif

int init_sockaddr_in(char *hostname, struct sockaddr_in *sin);
int client_process(int fd, char *hostname);
int send_request(int fd, char *hostname);
int recv_reply(int fd);


int main()
{
    struct sockaddr_in  server;
    SOCKET              sockfd;
    int                    recvMsgSize;
    
    // имя сервера, может быть вида "www."""".com" и т.д.
    char * srv =        "localhost"; 

#if defined WIN32
    // для Windows мы должны инициализировать Winsock
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(1,1), &wsa_data);
#endif

    // создаем сокет
    sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(sockfd == INVALID_SOCKET) {
        perror("Could not create socket.\n");
        return(SOCKET_ERROR);
    }

    // пробуем создать метаданные сервера
    recvMsgSize = init_sockaddr_in(srv, &server);
    if(recvMsgSize < 0)
    {
        perror("Could not initialize address.\n" );
        return(SOCKET_ERROR);
    }
    
    // пытаемся соединиться через сокет
    recvMsgSize = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
    if(recvMsgSize < 0)
    {
        perror("Error while connecting to server.\n");
        return(SOCKET_ERROR);
    }
    
    // посылаем сообщения на сервер и принимаем респонс
    recvMsgSize = client_process(sockfd, srv);
    
    printf("Program has ended successfully.");
    
#if defined WIN32
    // для Windows мы должны явно указать, что завершаем работу Winsock
    WSACleanup();
#endif
    
    return 0;
}

int init_sockaddr_in(char *hostname, struct sockaddr_in *sin)
{
    struct servent *serv;

    // утсанавливаем поинтер в памяти
    memset(sin, 0, sizeof(*sin));
    sin->sin_family = AF_INET;

    // проверяем, в каком виде записан адрес сервера: IP или "www.****.com"
#if defined LINUX
    if( !inet_aton( hostname, &sin->sin_addr ) )
#endif
    {
        // если запись в виде "www.****.com", ищем IP адрес
        struct hostent *host = gethostbyname(hostname);
        if(!host)
            return SOCKET_ERROR;

        sin->sin_addr.s_addr = *(unsigned long*)host->h_addr_list[0];
    }

    // получаем дефолтный порт для выбранного протокола
    serv = getservbyname("http", "tcp");
    if(!serv)
        return SOCKET_ERROR;

    sin->sin_port = serv->s_port;

    return EXIT_SUCCESS;
}

int client_process(int socketId, char *hostname)
{
    // посылаем данные    
    printf("Sending...\n");
    if(send_request(socketId, hostname))
    {
        // неудачно скастовали заклинание
        return SOCKET_ERROR;
    }
    
    // получаем ответ от сервера
    printf("Receiving reply...\n");
    if(recv_reply(socketId))
    {
        // ошибка магии
        return SOCKET_ERROR;
    }
    return EXIT_SUCCESS;
}

int send_request(int fd, char *hostname)
{
    // здесь мы должны сформировать запрос к серверу
    // запрос должен содержать стандартные заголовки и константы
    // типа таких: "GET /foo/bar/baz/ HTTP/1.1\r\nHost: www.myfoo.com Connection: close"
    // 
    char format[150] = "GET /foo/bar/baz/ HTTP/1.1\r\nHost: ";
    int length = 0;
    int ret = 0;

    strcat(format, hostname);
    strcat(format, "\r\nConnection: close\r\n\r\n");

    length = strlen(format);

    // посылаем запрос format длиной length через сокет fd  
    ret = send(fd, format, length, 0);
    if(ret != length )
        return SOCKET_ERROR;

    return EXIT_SUCCESS;
}

int recv_reply(int fd)
{
    fd_set readfds;
    
    // определяем буффер для входящих данных
    char buf[256];
    int length;

    while(1)
    {
        // инициализируем буффер
        FD_ZERO(&readfds);
        FD_SET(fd, &readfds);
        memset(buf, 0, sizeof(buf));

        // ждем прибытия данных с сервера
        if(select(fd+1, &readfds, NULL, NULL, NULL) < 0 )
        {
            // ошибочка в заклинании
            return SOCKET_ERROR;
        }
        
        // проверяем, если ли в дескрипторе переданные данные
        if(FD_ISSET(fd, &readfds))
        {
            // получаем их
            length = recv(fd, buf, sizeof(buf)-1, 0);
            if(length < 0)
                return SOCKET_ERROR;
            else if(length == 0)    // данные закончились
                break;
            
            // вываливаем полученный буфер куда хотим
            printf("%s\n", (char*)buf);
        }
    }
    return EXIT_SUCCESS;
}


Немного примечаний к волшебству.

Отношения с сокетами на этом уровне примерно одинаковые в Linux и в Windows. Я по долгу работы и в силу личных предпочтений пишу код в Windows (и чтобы не запусать лишний раз ресурсоемкий билд). Для этого в коде введен переключатель для определения условия компиляции. При отсутствии необходимости в нем, его легко можно удалить и компилироваться исключительно под выбранную ОС.

Wednesday, January 19, 2011

Измерение заряда батареи

Иногда перед каким-нибудь долгим, но важным процессом может понадобиться узнать уровень заряда батареи. Это бывает полезно, к примеру, когда пользователь собирается загрузить на девайс объемное обновление, и мы не хотим, чтобы этот завораживающий процесс прервался.

Для этой магии существует простое заклинание. В той Activity, где будет происходить длительный процесс, который лучше не прерывать, вставляем проверку:

private int checkBatteryLow(){
        BroadcastReceiver wIntentReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                // Получаем сообщение от девайса
                String wAction = intent.getAction();
                if(wAction.equals(Intent.ACTION_BATTERY_CHANGED))
                {
                    // Здесь можно информировать пользователя об изменении
                    // заряда батареи
                }
                else if(wAction.equals(Intent.ACTION_BATTERY_LOW))
                {
                    // Здесь можно информаровать пользователя о низком
                    // заряде батареи
                }
            }
        };

        // Создаем фильтры
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_BATTERY_CHANGED);
        filter.addAction(Intent.ACTION_BATTERY_LOW);

        // Подключаем получателя сообщений от девайса с установленными фильтрами
        Intent intent = this.registerReceiver(wIntentReceiver, filter);
        
        // Получем значение "level" из данных о заряде батареи
        Object l = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
        Object s = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
        int level = (Integer)l;
        int scale = (Integer)s;
        
        if (level >= 0 && scale > 0) {
            level = (level * 100) / scale;
        }
        
        // Отключаем получателя
        unregisterReceiver(wIntentReceiver);
        return level;
    }


После этого в нужном месте вызываем проверку и получаем из нее число, показывающее текущий заряд батареи (от 0 до 100). Это значение проверяем с надуманным пороговым значением и по езультатам проверки принимаем решение, запускать ли длительный процесс в приложении или заставить пользователя зарядить девайс.