Продолжаем! Для проекта NoWOL есть Android-приложение с виджетом.
Вообще, там всё весьма просто. Как будет возможность (не лень), загружу исходники в GitHub. Пока же там лежит вполне готовая к использованию APK'шка.
Из ключевых моментов: внутри используется библиотека Volley; работает приложение под Android 4.0 и выше (minSdkVersion 14, target 26).
После установки станет доступно основное приложение, а в разделе виджетов появится новый виджет с одноимённым названием «NoWOL».
Что касается приложения, то в нём доступно несколько состояний: неизвестно, онлайн, офлайн, нет ответа (от хоста). Обновить состояние можно нажав на кнопочку в верхней панели.
Если нажать на шестерёнку на верхней панели, всплывёт окно для указания IP хоста в сети. Следует обратить внимание на формат записи. Защиты от дурака там, к сожалению, не так много как хотелось бы. Вот хороший пример:
При добавлении виджета появится окно конфигуратора. Как видно, в нём нет указателя на адрес хоста. Он настраивается внутри приложения, поэтому сперва стоит запустить его.
Зато в конфигураторе можно выбрать задний фон (белый или чёрный) с альфа-каналом и уровень его прозрачности.
UI немного неочевиден и меняется в зависимости от выбора "галочки". Я знаю, так делать нехорошо.
В итоге, после добавления, виджет будет выглядеть как-то так (см. ниже). В его верхней строке может отображаться один из трёх возможных статусов. В отличии от приложения, там нет варианта "неизвестно". Зато вдалеке от домашней сети будет красоваться "нет ответа".
Забавный момент: если ПК в режиме ожидания, то статус будет или "онлайн" или "офлайн" в зависимости от того, мигнула ли лампочка в момент запроса состояния :)
Принудительно обновить статус можно ловко попав по верхнему правому углу, или через приложение — оно передаст полученные данные в виджет.
Вообще, статус обновляется сам каждые пол часа (если мне не изменяет память, но уж точно не чаще). Сделано это только для того, чтобы не сажать батарею телефона почём зря.
В общем-то на этом всё. Надеюсь, вам понравилось =)
Ох, давно хотел уже задокументировать это здесь, да всё времени не было. И вот, спустя полтора года…
Какую проблему решаем?
Стоит задача включать и выключать ПК нажимая на кнопки включения\выключения и сброса не руками, а через интернет (ну или через тоннель в интранет). В идеале спросонья через виджет в телефоне а-ля узнал погоду за бортом, включил ПК и можно идти ставить кофе.
Для этого было решено пилить какой-то контроллер Atmega с каким-то ethernet-свистком, чтобы он через оптопары замыкал кнопки включения. А заодно сообщал об успехе или неуспехе затеи.
Немного покопавшись я решил закупить основанную на enc28j60 приблуду для ардуины, а заодно и саму ардуину. Изучив особенности программирования enc28j60 руками (без высокоуровневых IDE и готовых библиотек) и бегло осмотрев errata, который, надо отметить, чуть ли не больше даташита, стало ясно, что на низком уровне рулить “этим” сущий ад и боль. Поэтому, будем собирать всё на базе добротного китайского клона Arduino nano или функционального аналога (лучше просто взять arduino, правда).
В итоге система будет состоять из основного модуля в виде связки Arduino+enc28j60 и выносного блока с оптопарами и резисторами.
В целом выглядеть и подключаться всё будет так:
Но перед тем, как начать…
Поставим себе пару пакетов:
1. dev-embedded/arduino
2. sci-electronics/eagle (ветка 7.x.x и лучше из нестабильной ветки)
Файлы для Eagle CAD находятся в директории eagle_files/m2Board/ .
Необходимые библиотеки в eagle_files/lib/ .
Для выносного модуля понадобится:
Part
Value
Device
Package
Library
Sheet
IC2
КР293КП4А
DIL08
ic-package
1
IC3
КР293КП4А
DIL08
ic-package
1
IN
PINHD-1X6
1X06
pinhead
1
OUT
PINHD-1X6
1X06
pinhead
1
R1
450Ω или 470Ω
R-EU_0204/5
0204/5
resistor
1
R2
450Ω или 470Ω
R-EU_0204/5
0204/5
resistor
1
R3
330Ω
R-EU_0204/5
0204/5
resistor
1
TO_MCU
PINHD-1X5
1X05
pinhead
1
По-поводу R1 и R2 резисторов, я собрал две пары, чтобы получилось 450 Ом в каждой. Думаю, что если взять просто пару штук на 470 Ом всё тоже будет работать. Но всё же обещать не буду.
Его принципиальная схема выглядит весьма просто:
Разводка выполняется на односторонней плате:
* 'cuz soviet works better
Если присмотреться, разводка слегка отличается от той, что приведена на первом рисунке. Тут я убрал перемычку.
Как вы уже поняли, в качестве оптопар используется отечественная схема КР293КП4А в корпусе DIP8. Внутри каждой микросхемы находится 2 оптопары. Одна из схем замыкает и размыкает кнопки (питание и сброс), а вторая говорит микроконтроллеру включена ли система, определяя это по светодиоду-индикатору питания. Если первую микросхему замыкает МК, то вторую — ПК. Через отдельный пин микроконтроллер зажигает индикатор питания сам, когда понимает, что и система (ПК) тоже его зажгла. Уж не знаю.. может это несколько избыточно и можно было сделать проще. Впрочем, получилось как получилось.
Примечание: наверное надо было навесить на вторую оптопару какие-то резисторы.. но блин, datasheet по ним такой немногословный. В общем как-то работает уже больше года, так что, пока всё не сгорело, имеет право на жизнь.
Если всё спаять и приправить должным количеством пыли, получится примерно следующее:
Про программирование Arduino
Теперь перейдём к программированию Arduino. Как говорилось ранее, предполагается использование Arduino Nano.
Я уже не помню что куда нажимать и подключать, чтобы это дело прошить но у меня есть скетч!
Для этого скетча понадобится библиотека ehtercard. И скетч и библиотека находятся в директории arduino_sketchbook/.
А вот и сам код:
#include <EtherCard.h>
#define PWR_PIN 6
#define RST_PIN 7
#define PWR_LED_IN 8
#define PWR_LED_OUT 5
int i;
// Зададим MAC адрес устройства
static byte mymac[] = { 0xDE,0xAD,0xBE,0xEE,0xEE,0xEF };
// Чем больше данных отображается на странице, тем больше требуется буфер для них
byte Ethernet::buffer[900];
BufferFiller bfill;
//Массив задействованных контактов для управления оптопарами
int LedPins[] = {PWR_PIN,RST_PIN};
//Массив для фиксации изменений
boolean PinUpdate = 0; //100-reset 200-power 5050-powerOFF
const char http_OK[] PROGMEM =
"HTTP/1.0 200 OK\r\n"
"Content-Type: text/html\r\n"
"Pragma: no-cache\r\n\r\n"
"\r\n"
"<html>"
"<head><title>"
"noWOL"
"</title><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"><style>"
"body { font-family: monospace;\n"
"background-color: #000000;\n"
"font-size: 30px;\n"
"font-weight: bold;\n"
"color: #b2b2b2;}\n"
"a:link, a:visited { display: inline-block;\n"
"color: #b2b2b2;\n"
"font-weight: 700;\n"
"text-decoration: none;\n"
"padding: .3em 2em;\n"
"outline: none;\n"
"border: 2px solid;\n"
"border-radius: 15px;\n"
"transition: 0.2s;}\n"
"a:hover {color: #fefefe;}\n"
"</style></head>"
"<body><center>"
;
const char http_Found[] PROGMEM =
"HTTP/1.0 302 Found\r\n"
"Location: /\r\n\r\n";
const char http_Unauthorized[] PROGMEM =
"HTTP/1.0 401 Unauthorized\r\n"
"Content-Type: text/html\r\n\r\n"
"401 Unauthorized";
void homePage()
{
bfill.emit_p(PSTR("$F"
" "
"$F<p>"
"<a href='?RESET=on'> RESET </a><p>" //D6
"<a href='?POWER0=on'> POWER </a><p>" //D7 short
"<a href='?POWER1=on'>POWER 5sec</a><br />"), //D7 long
//"PWR LED out D5 : $F <br /></body></html>"),
http_OK,
(digitalRead(PWR_LED_IN) == HIGH)?PSTR("<span style=\"color: #AA0000\"> ▄▄▄▄▄▄</span>"):PSTR("<span style=\"color: #00c600\"> ▄▄▄▄▄▄</span>")),//⬤
// (bitRead(PORTD,PWR_LED_OUT) == HIGH)?PSTR("HIGH"):PSTR("LOW")),
"</center></body>"
"</html>"
;
}
void pick_pin(int pin, int del) {
digitalWrite(pin,HIGH);
delay(del);
digitalWrite(pin,LOW);
PinUpdate = 0;
}
void setup()
{
// SET UART
// Serial.begin(9600);
//Change default ethercard CS - pin 8 to 10
//if (ether.begin(sizeof Ethernet::buffer, mymac, 10) == 0)
// Serial.println( "Access to Ethernet controller failed");
ether.begin(sizeof Ethernet::buffer, mymac, 10);
ether.dhcpSetup();
// if (!ether.dhcpSetup())
// Serial.println("DHCP routine failed");
//Print over UART configuration recieved:
// ether.printIp("IP: ", ether.myip);
// ether.printIp("GW: ", ether.gwip);
// ether.printIp("DNS: ", ether.dnsip);
for(i = 0; i < 2; i++) {
pinMode(LedPins[i],OUTPUT);
}
pinMode(PWR_LED_OUT,OUTPUT);
pinMode(PWR_LED_IN,INPUT_PULLUP); // LED-питания как вход
}
void loop()
{
delay(1);
if (digitalRead(PWR_LED_IN) == HIGH) { // если мать выключена
if (bitRead(PORTD,PWR_LED_OUT) == HIGH) digitalWrite(PWR_LED_OUT,LOW);
}
else if (bitRead(PORTD,PWR_LED_OUT) == LOW) {
digitalWrite(PWR_LED_OUT,HIGH);
}
switch (PinUpdate) {
case 0:
break;
case 1:
pick_pin(RST_PIN, 200);
break;
case 2:
pick_pin(PWR_PIN, 200);
break;
case 3:
pick_pin(PWR_PIN, 5050);
break;
}
word len = ether.packetReceive(); //Проверить ethernet пакеты
word pos = ether.packetLoop(len); //Проверить TCP пакеты
if (pos) {
bfill = ether.tcpOffset();
char *data = (char *) Ethernet::buffer + pos;
if (strncmp("GET /", data, 5) != 0) {
bfill.emit_p(http_Unauthorized);
}
else {
data += 5;
if (data[0] == ' ') {
homePage(); //Если обнаружено изменения на станице, то запускаем функцию
}
//"10" = количество символов "?RESET=on ".
else if (strncmp("?RESET=on ", data, 10) == 0) {
bfill.emit_p(http_Found);
PinUpdate = 1;
}
else if (strncmp("?POWER0=on ", data, 11) == 0) {
bfill.emit_p(http_Found);
PinUpdate = 2;
}
else if (strncmp("?POWER1=on ", data, 11) == 0) {
bfill.emit_p(http_Found);
PinUpdate = 3;
}
else {
//Страница не найдена
bfill.emit_p(http_Unauthorized);
}
}
ether.httpServerReply(bfill.position());
}
}
Теперь, когда всё что надо прошито, соединяем как указано на рисунке в начале публикации и подключаем это дело к сети. На машрутизаторе должен быть настроен DHCP сервер.
Мак-адрес нового устройства, как ясно из исходного кода, будет de:ad:be:ee:ee:ef. Лучше сразу назначить постоянную аренду. В OpenWRT с LuCI идём в:
«Сеть» → «DHCP и DNS» → вкладка «Основные настройки» → «Постоянные аренды» → «Добавить».
Пишем там как показано на рисунке:
Когда всё настроено и подключено, заходим на адрес http://192.168.1.100 и видим следующее:
Только вот квадратик будет красным, а не зеленым. Это и есть индикатор питания.
Назначение кнопок пояснять нет смысла. Последняя симулирует долгое нажатие, что приводит к принудительному отключению.
То, что всё это находится в интрасети и не отсвечивает в мир меня вполне устраивает, ведь при подключении по VPN доступ появляется и в Android-приложении и через WEB-интерфейс. Ах да! Для всего этого дела есть ещё и Android-приложение :) Но о нём во второй части.
Вредные советы
Как бы не использовать Arduino?
Лучше таки его использовать.
Но можно и спаять свою ардуину, с блекджеком и профусетками, чтобы она была поменьше и конкретно под этот проект.
Сразу оговорюсь, я наделал кучу ошибок и паял отдельные костыли чтобы всё сошлось.. потом переделал схему, потом ещё пару раз, и уже не помню на сколько там правильная разводка. Так что лучше внимательно изучить и прислать мне фидбек в виде линки на исправленную версию. Или апробировать, если всё правильно =)
Используется двусторонний текстолит. Перемычки тут не помогут.
Находится всё в директории eagle_files/atmegaBoard/ .
Список компонентов:
Part
Value
Device
Package
Library
Sheet
C1
22pF
C-EUC0603
C0603
resistor
1
C2
22pF
C-EUC0603
C0603
resistor
1
C3
10uF
CC0603
C0603
eagle-ltspice
1
C4
22uF
CC0603
C0603
eagle-ltspice
1
IC1
atmega328p
atmega328p
TQFP32-08
avr-6
1
IC3
LM1117IMPX-3.3
LM1117IMPX-3.3
SOT223
lm1117
1
JP1
PINHD-2X5
2X05
pinhead
1
JP3
PINHD-1X2
1X02
pinhead
1
Q1
16MHz
XTAL/S
QS
special
1
SV1
MA05-1
MA05-1
con-lstb
1
U1
USB""
USB-MICRO-SMD
MicroUSB
1
Схема принципиальная:
Сама разводка:
Что тут добавить.. земля на гребёнке справа. JP3 нужен для прошивки схемы. Прошивается она через Arduino IDE.. Кажется с помощью самой ардуины. Ну или через USBasp программатор. В общем, главное ничего не перепутать. А настройки должны быть вроде таких:
1)
2)
В следующей части будет обзор Android-приложения. APK файл находится в папке Android_APK.
Итого
У меня вся эта разработка шла не очень гладко, но получилось вполне сносно. Вот конечный результат в корпусе из спичек. Для антуража покрашено йодом.
Что касается дальнейшего развития этой инициативы, то я планирую добавить сюда ещё один выносной модуль для ещё одного ПК.
Для разных dpi можно воспользоваться встроенным инструментом Image Asset или сгенерировать их с помощью ImageMagic. Для второго варианта понадобиться базовое изображение, созданное для разрешение extra-extra-extra-high-density (xxxdpi).
Дальше используем скрипт:
#!/bin/bash
######################################
# As input we use file xxxhdpi file #
######################################
convert $1 -resize 75% $1_xxhdpi.png
convert $1 -resize 50% $1_xhdpi.png
convert $1_xhdpi.png -resize 75% $1_hdpi.png
convert $1 -resize 25% $1_mdpi.png
convert $1_mhdpi.png -resize 75% $1_ldpi.png
# Examples:
# 36x36 (0.75x) for low-density
# 48x48 (1.0x baseline) for medium-density
# 72x72 (1.5x) for high-density
# 96x96 (2.0x) for extra-high-density
# 144x144 (3.0x) for extra-extra-high-density
# 192x192 (4.0x) for extra-extra-extra-high-density
Сегодня мы будем создавать виджет, который может обновлять текст в нём по нажатию на одну из его четырёх кнопок. По нажатию на первые две будет вызываться MainActivity и обновлять текст на тот, что был передан в зависимости от того, какая из двух кнопок нажата. По нажатию на третью и четвёртую текст просто будет обновляться с помощью IntentSerive службы.
Код проекта доступен по ссылке https://github.com/developersu/MyWidgetNoConf/ .
Добавим классы MyIntentService и myWidget в основной package.
java→com.blogspot.developersu.mywidgetnoconf
→MyIntentService
→myWidget
Перейдём собственно к коду. Начнём с описания функционала виджета.
Мы будем использовать onUpdate и onEnabled плюс функцию для определения функций клавиш. Сохранением данных тестовом поле пренебрежем.
Итак, для начала код:
package com.blogspot.developersu.mywidgetnoconf;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
public class myWidget extends AppWidgetProvider {
private void setWidgetIntents(Context context, AppWidgetManager appWidgetManager, int appWidgetIds[]){
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.my_widget_layout);
for (int appWidgetId : appWidgetIds) {
Intent intentBtn = new Intent(context, MainActivity.class);
intentBtn.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intentBtn.putExtra("signature", "Button 1");
intentBtn.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP );
PendingIntent pi = PendingIntent.getActivity(context, appWidgetId+0, intentBtn, PendingIntent.FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.widgetBtn, pi);
/*===================================================================*/
// Set the same for button 2
Intent intentBtn2 = new Intent(context, MainActivity.class);
intentBtn2.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intentBtn2.putExtra("signature", "Button 2");
intentBtn2.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP );
PendingIntent pi2 = PendingIntent.getActivity(context, appWidgetId+1, intentBtn2, PendingIntent.FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.widgetBtn2, pi2);
/*===================================================================*/
Intent intentBtn3 = new Intent(context, MyIntentService.class);
intentBtn3.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intentBtn3.putExtra("signature", "Button 3");
PendingIntent pi3 = PendingIntent.getService(context, appWidgetId+2, intentBtn3, PendingIntent.FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.widgetBtn3, pi3);
/*===================================================================*/
Intent intentBtn4 = new Intent(context, MyIntentService.class);
intentBtn4.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intentBtn4.putExtra("signature", "Button 4");
PendingIntent pi4 = PendingIntent.getService(context, appWidgetId+3, intentBtn4, PendingIntent.FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.widgetBtn4, pi4);
appWidgetManager.updateAppWidget(appWidgetId, rv);
}
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
setWidgetIntents(context, appWidgetManager, appWidgetIds);
}
@Override
public void onEnabled(Context context) {
AppWidgetManager awm = AppWidgetManager.getInstance(context);
ComponentName compName = new ComponentName(context, myWidget.class);
int[] widgetIds = awm.getAppWidgetIds(compName); // избыточно, но оставим для наглядности
setWidgetIntents(context, awm, widgetIds);
}
}
У нас есть функция setWidgetIntents которая принимает context, AppWidgetManager и множество ID среди которых есть и ID нашего виджета. onUpdate — все данные у нас сразу имеются, так что просто передаём их в функцию onEnabled — создаём объект AppWidgetManager используя AppWidgetManager.getInstance(context), создаём ComponentName, который нам понадобится для вытаскивания передавая ему context и имя класса нашего виджета. IDs получаем через awm.getAppWidgetIds(compName). setWidgetIntents — опеределим “RemoteViews rv” чтобы через него присваивать кнопкам действия. Далее создадим цикл для всех полученных widget ID.
Присваивать каждой кнопке действия следует используя PendingIntent. Для него требуется Intent в который в первом и втором случае обращён к MainActivitiy.class, а в третьем и четвёртом — к MyIntentService.class.
В каждый такой Intent мы помещаем значение кнопки, которое будет передаваться. Это будет строка с именем “signature” и значением “Button N”, где N номер кнопки. Также передаём в него ID виджета, который инициировал вызов Intent.
Кроме того, в первых двух случаях мы определяем флаг Intent.FLAG_ACTIVITY_SINGLE_TOP который говорит системе, что Activity не должен запускаться, если экземпляр уже существует в стеке. Таким образом, если activity уже запущен, то приниматься Intent с этим флагом будет в методе onNewIntent(). Альтернативой этому может быть установка в манифесте launchMode в singleTop (тогда этот флаг не нужен):
Но этот вариант мы опустим. Трогать манифест не будем :)
Для обращения к сервису этот флаг нам не потребуется, т.к. сервисы работают совсем не так как обычные Activity.
В конце мы “создаём” PendingIntent обращаясь к getActivity или getService. Оба этих метода принимают 4 значения: context, requestCode, Intent и флаги. В requestCode мы будем заносить ID нашего виджет плюс какую-то цифру, чтобы определить каждый PendingIntent отдельно. Их стандартное поведение может поначалу ввести в заблуждение, но, если коротко, то для разных PendingIntent лучше бы иметь разные коды. Используемый флаг PendingIntent.FLAG_UPDATE_CURRENT говорит системе, что если описываемый PendingIntent уже существует, то следует лишь заменить его extra-данные (те, что мы добавляли используя метод addExtra).
Далее присваеваем каждой кнопке действие и “применяем измененеия”
Перейдём теперь к классу MyIntentService.
Класс наследует IntentService и реагирует ловит входящие Intent в onHandleIntent. При запуске вызыватся конструктор, порождается новая нить, которая после выполнения метода onHandleIntent() завершается.
Переходим к коду. Ничего, чтобы ещё хотелось выделить отдельно у меня нет.
Тут как не было элементов управления так и нет. При получении Intent будет вызываться или onCreate() (если приложение закрыто) или onNewIntent() (если экземляр уже существует). Определением того, что мы получили в прилетевшем Intent будет заниматься метод handleIntent(), который принимает Intent.
В случае, если Intent не пустой и содержит хоть какие-то extras, будем пытаться(!) брать из него предполагаемые данные, т.е. строку “signature” и ID виджета.
Сразу после этого, выведем тост (Toast) с полученой информацией. Если пользователь просто запускает приложение, то ему покажется уведомление с widget ID = 0 и string = null. Далее приложение проверит эти поля и если они отличны от вышеприведённых значений, то присвоит строку в TextView виджета. За присвоение отвечает метод sendBackToWidget().
package com.blogspot.developersu.mywidgetnoconf;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.RemoteViews;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private void handleIntent(Intent i){
String recievedString;
Bundle bndle = i.getExtras();
if (bndle != null) {
int awID = bndle.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
recievedString = bndle.getString("signature");
Toast.makeText(getApplicationContext(), "Got intent from: " + Integer.toString(awID) + " " + recievedString, Toast.LENGTH_SHORT).show();
if (awID != 0 && recievedString != null)
sendBackToWidget(awID, recievedString);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handleIntent(getIntent());
/* Почему мы вызываем handleIntent прямо тут? Потому, что если приложение закрыто а не работает в фоне, то при запуске его из виджета будет вызван
* onCreate. Очевидно, что если это приложение уже будет находиться в стеке, то вызываться будет что-то другое :) */
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
//setIntent(intent); // Удобно использовать, если бы наш Intent был объявлен глобальной переменной. Тогда бы мы просто переопределили его через setintent().
handleIntent(intent);
}
private void sendBackToWidget(int widgetID, String str){
AppWidgetManager awManager = AppWidgetManager.getInstance(getApplicationContext());
RemoteViews rv = new RemoteViews(getPackageName(), R.layout.my_widget_layout);
rv.setTextViewText(R.id.widgetText, str);
awManager.updateAppWidget(widgetID, rv);
}
}
Код не идеален и оставляет множество мест для оптимизаций. Впрочем, нет ничего хуже, чем преждевременная оптимизация, так что оставлю это вам.
Структурированно рассказать о том, как работает виджет сложновато, т. к. все составляющие очень тесно взаимосвязаны и всё это вызывает друг-друга туда-сюда как только хочет. Но давайте попробуем разобраться на живом примере. Полный код того, что получилось в конце страницы и на GitHub.
Виджеты бывают с «окном» конфигуратора и без него. Это «окно» является Activity, вызываемым при первом добавлении виджета на экран. Виджеты с ним и без него несколько отличаются в поведении. Такой конфигуратор нужен в основном для того, чтобы предопределить параметры для каждого нового добавляемого виджета. Это не значит, что добавляемые виджеты не могут быть одинаковыми, т.е. они могут, но без конфигуратора они будут одинаковыми гарантированно.
Итак конфигуратор, это обычная Activity которая получает параметры добавляемого виджета используя Intent, строго говоря она получает EXTRA_APPWIDGET_ID — ID добавляемого виджета и описывает какие параметры будут отображаться на виджете. По завершении, конфигуратор возвращает это значение назад и посылает RESULT_OK используя результирующий Intent. Как сказано ранее, конфигуратор должен не только вернуть эти данные, но и обновить объект RemoteViews, используемый для настройки отображаемых в виджете вещей, т. к. при добавлении виджета с помощью конфигуратора метод onUpdate() вызван не будет. Другими словами конфигуратор должен выставить необходимый текст в TextView виджета, добавить слушателей на кнопки виджета и т.п. Определяется Activity конфигуратора в файле настройки XML виджета (см.далее).
В случае, если конфигуратора нет, при добавлении виджета будет вызван метод onUpdate() в котором как правило и описывает большая часть того, что виджет должен делать. Подробнее о методах виджета поговорим чуть позже.
Задачу поставим так: сделать виджет с конфигуратором, который будет представлять собой текст и кнопку. По нажатии на кнопку будет вызываться конфигуратор. В конфигураторе будет EditText и кнопка для применения изменений. По нажатии на неё текст в виджете будет меняться на установленный в EditText конфигуратора.
Шаг 1. Для начала создадим проект с пустой MainActivity (File → New Project → …). Создадим внутри нашего проекта (пусть это будет в пакете java/widgets/) два java класса: MyWidget — сам будущий виджет MyWidgetConfig — окно конфигурации для виджета
Вернёмся к ним позже. Шаг 2. Создаём XML файл в res/xml/example_info.xml ('это наш AppWidget Provider) Это то, что будет описывать нюансы самого виджета и опишет связку будущего виджета с activity конфигуратора (то окно, которое будет запускаться при добавлении виджета на экран):
Пройдёмся по основным составляющим этого файла. configure определяет activity отвественную за конфигурацию виджета при добавлении и вообще. minHeight и minWidth определяет минимальные размеры для корректного отображения виджета. К сожалению, тут нет механизма в стиле “1 клеточка на 2 клеточки”. На странице документации по andoroid есть неплохое описание какие размеры должны быть (См. ссылки в конце статьи). В этом примере 1 клетка в высоту, 2 в ширину. updatePeriodMillis может быть установлен в 1800000 (30мин). Меньше вроде как не умеет (Вообще умеет, но требует реализации таймера). resizeMode определяет как пользователь может изменять размеры виджета. Можно установить в “none”, тогда размеры изменять будет нельзя. Можно в “horizontal”, можно в “vertical”, можно в оба одновременно “horizontal|vertical”. Названия говорят сами за себя. Шаг 3. Заходим в AndroidManifest.xml
Добавляем наш виджет и его кофигуратор в секцию <application>:
Шаг 4. Добавляем UI. Добавляем в layout/ два новых файла через New->XML->Layout XML
widget_main.xml
widget_main_config.xml
В widget_main_config.xml закидываем элементы EditText, Button со следующими ID:
configWidgetText
configWidgetBtn.
Шаг 5. Вернёмся к MyWidget.java
Виджеты являются в определённом роде BroadcastReciever которые обрабатывают определённые события. Как и BroadcastReciever, обрабатывают действия они методом onRevieve() который в дальнейшем обрабатывает данные другими методами. Тем не менее, виджет является наследником класса AppWidgetProvider (в свою очередь, наследник того самого BroadcastReciever), который всё несколько упрощает.
Рассмотрим что нам предлагает AppWidgetProvider: onEnabled — вызывается один раз, когда виджет создаётся (добавляется на экран). onUpdate — вызывается при обновлении виджета в заданный в XML файле интервал. Основной функционал виджета находится именно здесь. Как уже было сказано, если вам нужно вызывать этот метод чаще чем раз в 30 минут, следует добавить таймер (используя класс AlarmManager). onDeleted — вызывается, когда виджет удалён с экрана. onDisabled — вызывается, когда все виджеты удалены с экрана. Т.е. можно добавлять сколько угодно экземпляров этого виджета, и когда последний из добавленных будет удалён, этот метод сработает. В этом и отличие от onDeleted onRestored — вызывается при восстановлении AppWidget из бекапа. Откликается на ACTION_WIDGET_RESTORED. В обычном случае будет сразу вызван onUpdate. Даже не знаю как этот метод можно использовать. OnAppWidgetOptionsChanged — вызывается при изменении размеров виджета (да, если такие изменения вообще разрешены в XML настройках).
onReceive — вызывается для обработки событий другими методами.
Наследуем наш класс от AppWidgetProvider (extends AppWidgetProvider).
Жмём правой кнопкой на классе, потом Generate -> Override Methods…
Выбираем onUpdate и onDelete. Первый будет вызываться при обновлении виджета, второй — при удалении.
Для меня стало открытием, так что сейчас покажу :)
Для разнообразия, добавим всплывающее окно при удалении виджета через Toast:
Важно!
Этот метод будет вызываться в зависимости от настроек из res/xml/example_info.xml но при создании через конфигуратор он вызван не будет. Будет вызван onEnabled() (но и то как-то, по моим ощущениям, кривовато).
Для начала, установим что-нибудь в первое поле TextView. Какой смысл добавлять что-то в TextView, если это изменит текст через заданный интервал, тем более, что мы этот текст выставляем в конфигураторе? Смысла в этом нет. Просто это даст чуть более полную картину того, как работать с виджетами. В чём есть смысл, так это в определении кнопке действия, т.к. после перезагрузки устройства работать она перестанет (мы говорим о том, что если действие для кнопки будет определяться в конфигруаторе, то эта привязка перестанет работать после перезагурзки, т.к. в этом случае созданием виджета будет заниматься не конфигуратор а система). Но определять их надо в onEnabled, т.к. после перезагруки именно он и будет вызван.
Создадим экземпляр класса RemoteViews (референс: RemoteViews(String packageName, int layoutId) ). Где обращаемся к context из пренимаемых onUpdate параметров (берем оттуда имя) и layout самого виджета. Установим текст "Hello widget!" методом .setTextViewText (референс: setTextViewText(int viewId, CharSequence text)). После чего добавим ещё одну строку, которая укажет приложению на необходимость применить сделанные изменения. Такой финт не типичен для работы с Activity, но для виджетов каждое изменение в виджете должно сопровождаться обновлением.
Без такого обновления будет вылетать ошибка “Problem Loading Widget”. Проделывать эти действия надо со всеми appWidgetIds. Используем конструкцию for (foreach):
for (int appWidgetId : appWidgetIds) {
RemoteViews view = new RemoteViews(context.getPackageName(), R.layout.widget_main);
view.setTextViewText(R.id.widgetText, "Hello widget!");
appWidgetManager.updateAppWidget(appWidgetId, view);
}
Добавим метод onEnabled и назначим
кнопке на виджете действе: вызов конфигуратора по нажатию. В обычной
Activity достаточно было бы создать Intent, но в виджетах используется
несколько более сложная схема.
Итак, необходимо:
Создать Intent. В качетсве параметров — контекст и имя класса Activity-сонфигуратора с .class в конце.
Добавить в него ID виджета, который запрашивает вызов конфигуратора
(с помошью .putExtra). Ожидается переменная int для «поля»
AppWidgetManager.EXTRA_APPWIDGET_ID
Создать объект PendingIntent, который будет назначен кнопке для посылки-по-нажатию. См.аргументы ниже.
Попутно определим все необходимые параметры ещё раз (тут не плохо было бы подумать о том, как не повторяться с тем, что мы описывали в onUpdate). В отличии от onUpdate, принимаемое значение метода только context, так что надо вытащить откуда-то и ID. Для этого вводим объект класса ComponentName. Итак код:
@Override
public void onEnabled(Context context) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_main);
AppWidgetManager awm = AppWidgetManager.getInstance(context);
ComponentName compName = new ComponentName(context, MyWidget.class);
int[] widgetIds = awm.getAppWidgetIds(compName);
for (int widgetId : widgetIds) {
Intent intentBtnPwr = new Intent(context, MyWidgetConfig.class);
intentBtnPwr.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
PendingIntent pi = PendingIntent.getActivity(context, widgetId, intentBtnPwr, PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.widgetBtn, pi);
awm.updateAppWidget(widgetId, remoteViews);
}
}
Шаг 6. Перейдём к MyWidgetConfig.java
Объявляем переменные для всего класса.
EdText — поле ввода на экране конфигурации виджета
awManager — менеджер, который понадобиться для работы с виджетом,
thisContext — контекст
awID — переменная для хранения ID добавляемого виджета
Кнопку добавлять не будем, просто привяжем её к Listener и опишем необходимое поведение позже.
public class NoWolWidgetConfig extends Activity {
EditText EdText;
AppWidgetManager awManager;
Context thisContext;
int awID;
...
Наследуем класс от Activity, добавляем метод onCreate через Generate… (тем же способом, что описан выше).
Ну и сразу привязываем класс к layout.
Опишем Intent, чтобы получить информацию для текущей Activity о том, какой виджет вызвал эту Activity. В первый раз, при добавлении виджета, он помещается на экран и получает свой ID, после чего запускается конфигуратор. Если результат выполнения конфигуратора не успешен, или он был закрыт (кнопкой назад, например) виджет удаляется, т. к. не получает результрующего статуса RESULT_OK от конфигуратора. Также во входящем Intent конфигуратор получает ID экземпляра виджета и обязан по завершению передать его обратно, также используя механизм Intent.
Объявим Intent и привяжем его к полученному:
Intent intent = getIntent();
Создадим хранилище (Bundle) и выгрузим в него данные из полученного интента —присвоем awID значение ID виджета. В случае неудачи просто закроем конфигуратор и тогда виджет создан не будет.
Теперь определим контекст в переменную и с его использованием привяжем AppWidgetManager к нашему классу (нужен для такой же работы с виджетом, как и внутри самого кода виджета). И да, мы уже делали такое чуть ранее:
Назначим кнопке действие тем же способом, что и ранее мы проделывали в виджете в методе onEnabled.
Intent intentBtnPwr = new Intent(thisContext, MyWidgetConfig.class);
intentBtnPwr.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, awID);
PendingIntent pi = PendingIntent.getActivity(thisContext, awID, intentBtnPwr, PendingIntent.FLAG_UPDATE_CURRENT);
awRV.setOnClickPendingIntent(R.id.widgetBtn, pi);
После всего этого необходимо обновить виджет используя метод .updateAppWidget объекта класса AppWidgetManager. Он обновит в виджете только то, что мы изменили в виджете и не тронет другое. Проще говоря, смотрите на это как на кнопку «Применить».
awManager.updateAppWidget(awID, awRV);
Теперь мы передаём результат (ID виджета) в Intent в наш виджет и закрываем Activity конфигуратора. Так виджет будет знать, что конфигуратор успешно выполнился и будет отображен на экране. Точнее сказать, он не будет с него удалён, что произошло бы при закрытии конфигуратора (повторяюсь).
Intent resultIntent = new Intent();
resultIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, awID);
setResult(RESULT_OK, resultIntent);
finish();
Вот что у нас получилось:
Получилось вертикальное видео. На этом пожалуй всё. Как и обещал, код основных компонентов полностью:
manifests/AndroidManifest.xml
package com.blogspot.developersu.we.widgetexample;
/*
* It doesn't make sense to cover this code by any license, 'cause everything described here
* has been widely used by thousands of coders all around the world. So most likely I've already
* stolen this code in their eyes and you're going to steal it from me. LOL.
* Bye!
*
* Dmitry Isaenko,
* 2017, Russia
* https://developersu.blogspot.com/2017/07/androidwidget.html
*
*/
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
int awID;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
p.s. Есть небольшая проблема с MainActivity. Если он открыт (onPause, а не onActive, очевидно), и вы вызываете конфигуратор кнопкой на виджете, то MainActivity будет отображен после внесения изменений, хотя ожидается увидеть экран с виджетом. Виджет при этом отобразит сделанные в кнофигураторе изменения.
Бывает, что надо вытащить из полу-трупа телефона все контакты, которые в нём остались. Желательно в формате VCF. У меня вот такой аппарат не подключался к Wifi и никак не хотел синхронизироваться с гуглом. Очевидно, что будь у меня возможность синхронизации, проще всего было бы сделать экспорт из https://google.com/contacts. Ещё хороший вариант — сделать экспорт через стандартные средства в разделе контактов телефона, выбрав пункт 'Импорт/Экспорт контактов', но такая опция в последнее время встречается всё реже и есть не на всех андроедофонах.
В общем я задался целью получить файлик VCF со всеми данными. Для этого я сначала скачал файл базы данных sqlite. Это получилось сделать прямо на телефоне через Total Commander скопировав необходимый файл на карту памяти.
После этого я скопировал этот же файл на ПК.
Далее нам понадобится конвертер, благо распространяется он в виде обычного bash-скрипта. Скачать его можно отсюда: https://github.com/stachre/dump-contacts2db.
Теперь всё что остаётся сделать, так это поместить файл с базой и скрипт в одну директорию и запустить его:
В общем это всё. В интернете куча похожих рецептов связанных как с использованием dbconverter.sh, так и с помощью онлайн-конвертеров. С первым у меня не получилось из-за каких-то специфических заточек на использование в Ubuntu (просто не захотел разбираться), а онлайн конвертерам я не доверяю. На этом у меня всё.
Многие пользователи Android 4.4 столкнулись с интересной проблемой - перестали работать файловые менеджеры с картами памяти. Откровенно говоря, перестали работать функции копирования/удаления файлов, в то же время чтение происходит без нареканий. Одним из таких менеджеров стал и Total Commander, моя любимая программа со времён Windows Mobile.
Мне удалось получить некоторые комментарии по этому поводу от разработчика Total Commander - Кристиана Гислера (Christian Ghisler).
В частности он пишет "Total Commander использует хаки для создания папок и файлов. К сожалению, Android 4.4 блокирует хак для создания новых файлов на SD карте". Таким образом, Кристиан приводит несколько мозможных путей обхода этой проблемы: "Вы можете использовать следующие варианты:
Использовать предустановленый файловый менеджер, если он есть, например «Мои Файлы» в смартфонах Samsung
Использовать Total Commander 2.05 beta - он может писать в свою собственную частную директорию на карте памяти, но больше - никуда
Получить Root на устройстве и использовать бесплатную утилиту sdfix из Play Маркета, чтобы вернуть возможность записи для всех приложений.
Подождать Android 5".
Я также выяснил, что в интернете распространён способ (root таки нужен) вручную прописать правила для раблокировки возможностей чтения/записи для приложений. Для этого нужно добавить строку в слдующем файле: