Skip to content

Commit

Permalink
[TIMOB-24510] Android: Support for custom quick settings tiles. (#9310)
Browse files Browse the repository at this point in the history
* Support for QuickSettings tiles.

* Add documentation.

* Formatting.
Optimize default label and icon code in build script.

* Fix indentation.

* Fix indentation again.

* Add constants.
Fix parameters.

* Add names.

* Extract dictionary with dialog show params.
Add returns.

* Fix return types.

* More indentation.

* Last attempt.

* Update QuickSettingsService.yml

* Change serviceParser.
Fix default icon code.
Fix parity.html template.

* Docs formatting.
  • Loading branch information
ypbnv authored and sgtcoolguy committed Nov 10, 2017
1 parent dc4ea45 commit 50f7a4c
Show file tree
Hide file tree
Showing 10 changed files with 623 additions and 10 deletions.
58 changes: 49 additions & 9 deletions android/cli/commands/_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -3262,13 +3262,17 @@ AndroidBuilder.prototype.generateJavaFiles = function generateJavaFiles(next) {
// generate the JavaScript-based services
if (android && android.services) {
const serviceTemplate = fs.readFileSync(path.join(this.templatesDir, 'JSService.java')).toString(),
intervalServiceTemplate = fs.readFileSync(path.join(this.templatesDir, 'JSIntervalService.java')).toString();
intervalServiceTemplate = fs.readFileSync(path.join(this.templatesDir, 'JSIntervalService.java')).toString(),
quickSettingsServiceTemplate = fs.readFileSync(path.join(this.templatesDir, 'JSQuickSettingsService.java')).toString();
Object.keys(android.services).forEach(function (name) {
const service = android.services[name];
let tpl = serviceTemplate;
if (service.type === 'interval') {
tpl = intervalServiceTemplate;
this.logger.debug(__('Generating interval service class: %s', service.classname.cyan));
} else if (service.type === 'quicksettings') {
tpl = quickSettingsServiceTemplate;
this.logger.debug(__('Generating quick settings service class: %s', service.classname.cyan));
} else {
this.logger.debug(__('Generating service class: %s', service.classname.cyan));
}
Expand Down Expand Up @@ -3440,6 +3444,30 @@ AndroidBuilder.prototype.generateTheme = function generateTheme(next) {
next();
};

function serviceParser(serviceNode) {
// add service attributes
const resultService = {};
appc.xml.forEachAttr(serviceNode, function (attr) {
resultService[attr.localName] = attr.value;
});
appc.xml.forEachElement(serviceNode, function (node) {
if (!resultService[node.tagName]) {
resultService[node.tagName] = [];
}
// create intent-filter instance
const intentFilter = {};
const action = [];
intentFilter['action'] = action;
// add atrributes from parent
appc.xml.forEachElement(node, function (intentFilterAaction) {
intentFilter['action'].push(appc.xml.getAttr(intentFilterAaction, 'android:name'));
});
// add intent filter object to array
resultService[node.tagName].push(intentFilter);
});
return resultService;
}

AndroidBuilder.prototype.generateAndroidManifest = function generateAndroidManifest(next) {
if (!this.forceRebuild && fs.existsSync(this.androidManifestFile)) {
return next();
Expand Down Expand Up @@ -3659,14 +3687,26 @@ AndroidBuilder.prototype.generateAndroidManifest = function generateAndroidManif
tiappServices && Object.keys(tiappServices).forEach(function (filename) {
const service = tiappServices[filename];
if (service.url) {
const s = {
'name': this.appid + '.' + service.classname
};
Object.keys(service).forEach(function (key) {
if (!/^(type|name|url|options|classname|android:name)$/.test(key)) {
s[key.replace(/^android:/, '')] = service[key];
}
});
let s = {};
if (service.type === 'quicksettings') {
const serviceName = this.appid + '.' + service.classname;
const icon = '@drawable/' + (service.icon || this.tiapp.icon).replace(/((\.9)?\.(png|jpg))$/, '');
const label = service.label || this.tiapp.name;
const serviceXML = ejs.render(fs.readFileSync(path.join(this.templatesDir, 'QuickService.xml')).toString(), {
serviceName: serviceName,
icon: icon,
label: label
});
const doc = new DOMParser().parseFromString(serviceXML, 'text/xml');
s = serviceParser(doc.firstChild);
} else {
s.name = this.appid + '.' + service.classname;
Object.keys(service).forEach(function (key) {
if (!/^(type|name|url|options|classname|android:name)$/.test(key)) {
s[key.replace(/^android:/, '')] = service[key];
}
});
}
finalAndroidManifest.application.service || (finalAndroidManifest.application.service = {});
finalAndroidManifest.application.service[s.name] = s;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import android.content.pm.PackageManager;
import android.os.Build;
import android.service.quicksettings.Tile;
import org.appcelerator.kroll.KrollDict;
import org.appcelerator.kroll.KrollFunction;
import org.appcelerator.kroll.KrollModule;
Expand Down Expand Up @@ -275,6 +276,10 @@ public class AndroidModule extends KrollModule
@Kroll.constant public static final int NAVIGATION_MODE_STANDARD = ActionBar.NAVIGATION_MODE_STANDARD;
@Kroll.constant public static final int NAVIGATION_MODE_TABS = ActionBar.NAVIGATION_MODE_TABS;

@Kroll.constant public static final int TILE_STATE_UNAVAILABLE = Tile.STATE_UNAVAILABLE;
@Kroll.constant public static final int TILE_STATE_INACTIVE = Tile.STATE_INACTIVE;
@Kroll.constant public static final int TILE_STATE_ACTIVE = Tile.STATE_ACTIVE;

@Kroll.constant public static final int WAKE_LOCK_PARTIAL = PowerManager.PARTIAL_WAKE_LOCK;
@Kroll.constant public static final int WAKE_LOCK_FULL = PowerManager.FULL_WAKE_LOCK;
@Kroll.constant public static final int WAKE_LOCK_SCREEN_DIM = PowerManager.SCREEN_DIM_WAKE_LOCK;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package ti.modules.titanium.android.quicksettings;

import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.graphics.drawable.Icon;
import android.service.quicksettings.TileService;

import org.appcelerator.kroll.KrollDict;
import org.appcelerator.kroll.KrollRuntime;
import org.appcelerator.kroll.annotations.Kroll;
import org.appcelerator.kroll.common.Log;
import org.appcelerator.titanium.TiApplication;
import org.appcelerator.titanium.TiC;
import org.appcelerator.titanium.proxy.IntentProxy;
import org.appcelerator.titanium.proxy.ServiceProxy;
import org.appcelerator.titanium.view.TiDrawableReference;

@TargetApi(24)
@Kroll.proxy
public class QuickSettingsServiceProxy extends ServiceProxy {

private static final String TAG = "QuickSettingsService";

private TileService tileService;
//workaround for dealing with Icon class
private Object pathObject = null;
private AlertDialog.Builder builder;

public QuickSettingsServiceProxy(TileService serviceInstance) {
tileService = serviceInstance;
}

//Update the tile with the latest changes
@Kroll.method
public void updateTile() {
tileService.getQsTile().updateTile();
}

//Setting Tile's icon
@Kroll.method
public void setIcon(Object path) {
tileService.getQsTile().setIcon(Icon.createWithBitmap(TiDrawableReference.fromObject(TiApplication.getAppRootOrCurrentActivity(),path).getBitmap()));
pathObject = path;
}

//Setting Tile's state
@Kroll.method
public void setState(int state) {
tileService.getQsTile().setState(state);
}

//Setting Tile's label
@Kroll.method
public void setLabel(String label) {
tileService.getQsTile().setLabel(label);
}

//Getting Tile'c icon
@Kroll.method
public Object getIcon() {
return pathObject;
}

//Getting Tile's state
@Kroll.method
public int getState() {
return tileService.getQsTile().getState();
}

//Getting Tile's label
@Kroll.method
public String getLabel() {
return tileService.getQsTile().getLabel().toString();
}

//Checks if the lock screen is showing.
@Kroll.method
public final boolean isLocked() {
return tileService.isLocked();
}

//Checks if the device is in a secure state.
@Kroll.method
public final boolean isSecure() {
return tileService.isSecure();
}

//Used to show a dialog.
@Kroll.method
public void showDialog(KrollDict krollDictionary) {
tileService.showDialog(createDialogFromDictionary(krollDictionary));
}

//Start an activity while collapsing the panel.
@Kroll.method
public void startActivityAndCollapse(IntentProxy intent) {
tileService.startActivityAndCollapse(intent.getIntent());
}

//Prompts the user to unlock the device before executing the JS file.
@Kroll.method
final void unlockAndRun(final String jsToEvaluate) {
tileService.unlockAndRun(new Runnable() {
@Override
public void run() {
KrollRuntime.getInstance().evalString(jsToEvaluate);
}
});
}

private Dialog createDialogFromDictionary(KrollDict krollDict) {
builder = new AlertDialog.Builder(tileService.getApplicationContext());
String[] buttonText = null;
if (krollDict.containsKey(TiC.PROPERTY_TITLE)) {
builder.setTitle(krollDict.getString(TiC.PROPERTY_TITLE));
}
if (krollDict.containsKey(TiC.PROPERTY_MESSAGE)) {
builder.setMessage(krollDict.getString(TiC.PROPERTY_MESSAGE));
}
if (krollDict.containsKey(TiC.PROPERTY_BUTTON_NAMES)) {
buttonText = krollDict.getStringArray(TiC.PROPERTY_BUTTON_NAMES);
} else if (krollDict.containsKey(TiC.PROPERTY_OK)) {
buttonText = new String[]{krollDict.getString(TiC.PROPERTY_OK)};
}
if (krollDict.containsKey(TiC.PROPERTY_OPTIONS)) {
String[] optionText = krollDict.getStringArray(TiC.PROPERTY_OPTIONS);
int selectedIndex = krollDict.containsKey(TiC.PROPERTY_SELECTED_INDEX) ? krollDict.getInt(TiC.PROPERTY_SELECTED_INDEX) : -1;
if(selectedIndex >= optionText.length){
Log.d(TAG, "Ooops invalid selected index specified: " + selectedIndex, Log.DEBUG_MODE);
selectedIndex = -1;
}

processOptions(optionText, selectedIndex);
}

if (buttonText != null) {
processButtons(buttonText);
}
return builder.create();
}

private void processOptions(String[] optionText,int selectedIndex)
{
builder.setSingleChoiceItems(optionText, selectedIndex , new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
KrollDict eventDictionary = new KrollDict();
eventDictionary.put(TiC.PROPERTY_ITEM_INDEX,which);
fireEvent(TiC.EVENT_TILE_DIALOG_OPTION_SELECTED, eventDictionary);
}
});
}

private void processButtons(String[] buttonText)
{
builder.setPositiveButton(null, null);
builder.setNegativeButton(null, null);
builder.setNeutralButton(null, null);
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog)
{
dialog.dismiss();
fireEvent(TiC.EVENT_TILE_DIALOG_CANCELED,null);
}
});

for (int id = 0; id < buttonText.length; id++) {
String text = buttonText[id];

switch (id) {
case 0:
builder.setPositiveButton(text, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
fireEvent(TiC.EVENT_TILE_DIALOG_POSITIVE, null);
}
});
break;
case 1:
builder.setNeutralButton(text, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
fireEvent(TiC.EVENT_TILE_DIALOG_NEUTRAL, null);
}
});
break;
case 2:
builder.setNegativeButton(text, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
fireEvent(TiC.EVENT_TILE_DIALOG_NEGATIVE, null);
}
});
break;
default:
Log.e(TAG, "Only 3 buttons are supported");
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package ti.modules.titanium.android.quicksettings;

import android.os.Build;
import android.service.quicksettings.TileService;
import android.support.annotation.RequiresApi;
import org.appcelerator.kroll.KrollRuntime;
import org.appcelerator.kroll.util.KrollAssetHelper;
import org.appcelerator.titanium.TiC;
import org.appcelerator.titanium.proxy.ServiceProxy;

@RequiresApi(api = Build.VERSION_CODES.N)
public class TiJSQuickSettingsService extends TileService{

private final ServiceProxy proxy;

public TiJSQuickSettingsService(String url) {
//create a proxy for this service
proxy = new QuickSettingsServiceProxy(this);
//get the source to be run
String source = KrollAssetHelper.readAsset(url);
//run the module
KrollRuntime.getInstance().runModule(source, url, proxy) ;
}

//Called when the user clicks on this tile.
@Override
public void onClick() {
proxy.fireEvent(TiC.EVENT_CLICK, null);
}

//Called when the user adds this tile to Quick Settings.
@Override
public void onTileAdded() {
proxy.fireEvent(TiC.EVENT_TILE_ADDED, null);
}

//Called when the user removes this tile from Quick Settings.
@Override
public void onTileRemoved() {
proxy.fireEvent(TiC.EVENT_TILE_REMOVED, null);
}

//Called by the system to notify a Service that it is no longer used and is being removed.
@Override
public void onDestroy() {
proxy.fireEvent(TiC.EVENT_DESTROY, null);
}

//Called when this tile moves into a listening state.
@Override
public void onStartListening() {
proxy.fireEvent(TiC.EVENT_START_LISTENING, null);
}

//Called when this tile moves out of the listening state.
@Override
public void onStopListening() {
proxy.fireEvent(TiC.EVENT_STOP_LISTENING, null);
}
}
9 changes: 9 additions & 0 deletions android/templates/build/JSQuickSettingsService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package <%- appid %>;

import ti.modules.titanium.android.quicksettings.TiJSQuickSettingsService;

public final class <%- service.classname %> extends TiJSQuickSettingsService {
public <%- service.classname %>() {
super("<%- service.url %>");
}
}
5 changes: 5 additions & 0 deletions android/templates/build/QuickService.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<service android:name="<%- serviceName %>" android:label="<%- label %>" android:icon="<%- icon %>" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE"/>
</intent-filter>
</service>

0 comments on commit 50f7a4c

Please sign in to comment.