Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TIMOB-24510] Android: Support for custom quick settings tiles. #9310

Merged
merged 25 commits into from
Nov 10, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 54 additions & 7 deletions android/cli/commands/_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -3176,12 +3176,16 @@ AndroidBuilder.prototype.generateJavaFiles = function generateJavaFiles(next) {
if (android && android.services) {
var serviceTemplate = fs.readFileSync(path.join(this.templatesDir, 'JSService.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) {
var service = android.services[name],
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 @@ -3351,6 +3355,28 @@ AndroidBuilder.prototype.generateTheme = function generateTheme(next) {
next();
};

var serviceParser = function(serviceNode, result) {
//add service attributes
appc.xml.forEachAttr(serviceNode, function (attr) {
result[attr.localName] = attr.value;
});
appc.xml.forEachElement(serviceNode, function (node) {
if (!result[node.tagName]) {
result[node.tagName] = [];
}
//create intent-filter instance
var intentFilter = {};
var 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
result[node.tagName].push(intentFilter);
});
}

AndroidBuilder.prototype.generateAndroidManifest = function generateAndroidManifest(next) {
if (!this.forceRebuild && fs.existsSync(this.androidManifestFile)) {
return next();
Expand Down Expand Up @@ -3568,14 +3594,35 @@ AndroidBuilder.prototype.generateAndroidManifest = function generateAndroidManif
tiappServices && Object.keys(tiappServices).forEach(function (filename) {
var service = tiappServices[filename];
if (service.url) {
var 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];
var s;
if (service.type == 'quicksettings') {
s = {};
var serviceName = this.appid + '.' + service.classname;
var icon = '@drawable/' + this.tiapp.icon.replace(/((\.9)?\.(png|jpg))$/, '');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var icon = '@drawable/' + (service.label || this.tiapp).icon.replace(/((\.9)?\.(png|jpg))$/, '');

if (service.icon) {
icon = '@drawable/' + service.icon.replace(/((\.9)?\.(png|jpg))$/, '');
}
});
var label = this.tiapp.name;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var label = service.label || this.tiapp.name;

if (service.label) {
label = service.label;
}
serviceXML = ejs.render(fs.readFileSync(path.join(this.templatesDir, 'QuickService.xml')).toString(), {
serviceName: serviceName,
icon: icon,
label: label
});
doc = new DOMParser().parseFromString(serviceXML,"text/xml");
serviceParser(doc.firstChild,s);
} 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 @@ -274,6 +275,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;

protected RProxy r;
private static final int REQUEST_CODE = 99;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
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))
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix {

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing of ,

same below

}
});
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>