package com.kuxhausen.huemore.persistence; import android.content.Context; import android.database.Cursor; import android.util.Pair; import com.kuxhausen.huemore.state.BulbState; import com.kuxhausen.huemore.state.BulbState.Alert; import com.kuxhausen.huemore.state.BulbState.Effect; import com.kuxhausen.huemore.state.Event; import com.kuxhausen.huemore.state.Group; import com.kuxhausen.huemore.state.Mood; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; public class HueUrlEncoder { /** * zero indexed * */ public final static Integer PROTOCOL_VERSION_NUMBER = 4; public static String encode(Mood mood) { return encodeLegacy(mood, null, null); } public static String encode(Mood m, Group g, Integer brightness, Context c) { Integer[] legacyArray = new Integer[50]; String[] projections = {Definitions.NetBulbColumns.DEVICE_ID_COLUMN}; for (Long l : g.getNetworkBulbDatabaseIds()) { String[] selectionArgs = {"" + l, "" + Definitions.NetBulbColumns.NetBulbType.PHILIPS_HUE}; Cursor cursor = c.getContentResolver().query( Definitions.NetBulbColumns.URI, projections, Definitions.NetBulbColumns._ID + " =? AND " + Definitions.NetBulbColumns.TYPE_COLUMN + " =?", selectionArgs, null ); if (cursor.moveToFirst()) { String s = cursor.getString(0); int hueBulbNum = Integer.parseInt(s); legacyArray[hueBulbNum] = hueBulbNum; } cursor.close(); } return encodeLegacy(m, legacyArray, brightness); } public static String encodeLegacy(Mood m, Integer[] bulbsAffected, Integer brightness) { Mood mood = m.clone(); if (mood == null) { return ""; } ManagedBitSet mBitSet = new ManagedBitSet(); // Set 3 bit protocol version mBitSet.addNumber(PROTOCOL_VERSION_NUMBER, 3); // Flag if optional bulblist included mBitSet.incrementingSet(bulbsAffected != null); // 50 bit optional bulb inclusion flags if (bulbsAffected != null) { boolean[] bulbs = new boolean[50]; for (Integer i : bulbsAffected) { if (i != null) { bulbs[i - 1] = true; } } for (int i = 0; i < bulbs.length; i++) { mBitSet.incrementingSet(bulbs[i]); } } /** optional total brightness added in Protocol_Version_Number==3 **/ { /** * The brightness value to set the light to. Brightness is a scale from 0 (the minimum the * light is capable of) to 255 (the maximum). Note: a brightness of 0 is not off. */ // Flag if optional brightness included mBitSet.incrementingSet(brightness != null); // If optional brightness included, write it's 8 bits if (brightness != null) { mBitSet.addNumber(brightness, 8); } } // Set 6 bit number of channels mBitSet.addNumber(mood.getNumChannels(), 6); addTimingRepeatPolicy(mBitSet, mood); ArrayList<Integer> timeArray = generateTimesArray(mood); // Set 6 bit number of timestamps mBitSet.addNumber(timeArray.size(), 6); // Set variable size list of 20 bit timestamps for (Integer i : timeArray) { mBitSet.addNumber(i, 20); } ArrayList<BulbState> stateArray = generateStatesArray(mood); // Set 12 bit number of states mBitSet.addNumber(stateArray.size(), 12); for (BulbState state : stateArray) { addState(mBitSet, state); } // Set 12 bit number of events mBitSet.addNumber(mood.getEvents().length, 12); addListOfEvents(mBitSet, mood, timeArray, stateArray); // Set 20 bit timestamps representing the loopIterationTimeLength mBitSet.addNumber(Utils.toDeciSeconds(mood.getLoopMilliTime()), 20); String encoded = mBitSet.getBase64Encoding(); // bug fix against newlines in encodeLegacy String corrected = encoded.replaceAll(("" + (char) 10), ""); return corrected; } /** * Set 8 bit timing repeat policy * */ private static void addTimingRepeatPolicy(ManagedBitSet mBitSet, Mood mood) { // 1 bit timing addressing reference mode boolean isRelativeToMidnight = (mood.getTimingPolicy() == Mood.TimingPolicy.DAILY); mBitSet.incrementingSet(isRelativeToMidnight); // 7 bit timing repeat number. Clients only support 0 (no loops) or 127 (infinite loops). boolean isLooping = (mood.getTimingPolicy() == Mood.TimingPolicy.DAILY || mood.getTimingPolicy() == Mood.TimingPolicy.LOOPING); int numLoops = isLooping ? 127 : 0; mBitSet.addNumber(numLoops, 7); } /** * Set variable length state * */ private static void addState(ManagedBitSet mBitSet, BulbState bs) { /** Put 9 bit properties flags **/ { // On/OFF flag always include in v1 implementation 1 mBitSet.incrementingSet(bs.getOn() != null); // Put bri flag mBitSet.incrementingSet(bs.get255Bri() != null); // Put hue flag mBitSet.incrementingSet(false); // Put sat flag mBitSet.incrementingSet(false); // Put xy flag mBitSet.incrementingSet(bs.hasXY()); // Put ct flag mBitSet.incrementingSet(bs.getMiredCT() != null); // Put alert flag mBitSet.incrementingSet(bs.getAlert() != null); // Put effect flag mBitSet.incrementingSet(bs.getEffect() != null); // Put transitiontime flag mBitSet.incrementingSet(bs.getTransitionTime() != null); } /** Put on bit **/ if (bs.getOn() != null) { mBitSet.incrementingSet(bs.getOn()); } /** Put 8 bit bri **/ if (bs.get255Bri() != null) { mBitSet.addNumber(bs.get255Bri(), 8); } /** Put 64 bit xy **/ if (bs.hasXY()) { int x = Float.floatToIntBits(bs.getXY()[0]); mBitSet.addNumber(x, 32); int y = Float.floatToIntBits(bs.getXY()[1]); mBitSet.addNumber(y, 32); } /** Put 9 bit ct **/ if (bs.getMiredCT() != null) { mBitSet.addNumber(bs.getMiredCT(), 9); } /** Put 2 bit alert **/ if (bs.getAlert() != null) { int value = 0; if (bs.getAlert().equals(Alert.NONE)) { value = 0; } else if (bs.getAlert().equals(Alert.FLASH_ONCE)) { value = 1; } else if (bs.getAlert().equals(Alert.FLASH_30SEC)) { value = 2; } mBitSet.addNumber(value, 2); } /** Put 4 bit effect **/ // three more bits than needed, reserved for future API // functionality if (bs.getEffect() != null) { int value = 0; if (bs.getEffect().equals(Effect.NONE)) { value = 0; } else if (bs.getEffect().equals(Effect.COLORLOOP)) { value = 1; } mBitSet.addNumber(value, 4); } /** Put 16 bit transitiontime **/ if (bs.getTransitionTime() != null) { mBitSet.addNumber(bs.getTransitionTime(), 16); } } /** * Set variable length list of variable length events * */ private static void addListOfEvents(ManagedBitSet mBitSet, Mood mood, ArrayList<Integer> timeArray, ArrayList<BulbState> stateArray) { String[] bulbStateToStringArray = new String[stateArray.size()]; for (int i = 0; i < stateArray.size(); i++) { bulbStateToStringArray[i] = stateArray.get(i).toString(); } ArrayList<String> bulbStateToStringList = new ArrayList<String>(Arrays.asList(bulbStateToStringArray)); for (Event e : mood.getEvents()) { // add channel number mBitSet.addNumber(e.getChannel(), getBitLength(mood.getNumChannels())); // add timestamp lookup number mBitSet.addNumber(timeArray.indexOf(e.getLegacyTime()), getBitLength(timeArray.size())); // add mood lookup number mBitSet.addNumber(bulbStateToStringList.indexOf(e.getBulbState().toString()), getBitLength(stateArray.size())); } } /** * calulate number of bits needed to address this many addresses * */ private static int getBitLength(int addresses) { int length = 0; while (addresses != 0) { addresses = addresses >>> 1; length++; } return length; } private static ArrayList<Integer> generateTimesArray(Mood mood) { HashSet<Integer> timeset = new HashSet<Integer>(); for (Event e : mood.getEvents()) { timeset.add(e.getLegacyTime()); } ArrayList<Integer> timesArray = new ArrayList<Integer>(); timesArray.addAll(timeset); return timesArray; } private static ArrayList<BulbState> generateStatesArray(Mood mood) { HashMap<String, BulbState> statemap = new HashMap<String, BulbState>(); for (Event e : mood.getEvents()) { statemap.put(e.getBulbState().toString(), e.getBulbState()); } ArrayList<BulbState> statesArray = new ArrayList<BulbState>(); statesArray.addAll(statemap.values()); return statesArray; } private static BulbState extractState(ManagedBitSet mBitSet) { BulbState bs = new BulbState(); /** * On, Bri, Hue, Sat, XY, CT, Alert, Effect, Transitiontime */ boolean[] propertiesFlags = new boolean[9]; /** Get 9 bit properties flags **/ for (int j = 0; j < 9; j++) { propertiesFlags[j] = mBitSet.incrementingGet(); } /** Get on bit **/ if (propertiesFlags[0]) { bs.setOn(mBitSet.incrementingGet()); } /** Get 8 bit bri **/ if (propertiesFlags[1]) { bs.set255Bri(mBitSet.extractNumber(8)); } Integer hue = null; /** Get 16 bit hue **/ if (propertiesFlags[2]) { hue = mBitSet.extractNumber(16); } Integer sat = null; /** Get 8 bit sat **/ if (propertiesFlags[3]) { sat = mBitSet.extractNumber(8); } // convert hue, sat into xy approximation if (hue != null && sat != null) { float[] hsv = {(hue * 360) / 65535, sat / 255f, 1}; float[] input = {hsv[0] / 360f, hsv[1]}; bs.setXY(Utils.hsTOxy(input)); } /** Get 64 bit xy **/ if (propertiesFlags[4]) { Float x = Float.intBitsToFloat(mBitSet.extractNumber(32)); Float y = Float.intBitsToFloat(mBitSet.extractNumber(32)); bs.setXY(new float[]{x, y}); } /** Get 9 bit ct **/ if (propertiesFlags[5]) { bs.setMiredCT(mBitSet.extractNumber(9)); } /** Get 2 bit alert **/ if (propertiesFlags[6]) { int value = mBitSet.extractNumber(2); switch (value) { case 0: bs.setAlert(Alert.NONE); break; case 1: bs.setAlert(Alert.FLASH_ONCE); break; case 2: bs.setAlert(Alert.FLASH_30SEC); break; } } /** Get 4 bit effect **/ // three more bits than needed, reserved for future API // functionality if (propertiesFlags[7]) { int value = mBitSet.extractNumber(4); switch (value) { case 0: bs.setEffect(Effect.NONE); break; case 1: bs.setEffect(Effect.COLORLOOP); break; } } /** Get 16 bit transitiontime **/ if (propertiesFlags[8]) { int value = mBitSet.extractNumber(16); bs.setTransitionTime(value); } return bs; } public static Pair<Integer[], Pair<Mood, Integer>> decode(String code) throws InvalidEncodingException, FutureEncodingException { try { Mood.Builder moodBuilder = new Mood.Builder(); int numChannels = 0; ArrayList<Integer> bList = new ArrayList<Integer>(); Integer brightness = null; ManagedBitSet mBitSet = new ManagedBitSet(code); // 3 bit encoding version int encodingVersion = mBitSet.extractNumber(3); // 1 bit optional bulb inclusion flags flag boolean hasBulbs = mBitSet.incrementingGet(); if (hasBulbs) { // 50 bits of optional bulb inclusion flags for (int i = 0; i < 50; i++) { if (mBitSet.incrementingGet()) { bList.add(i + 1); } } } if (encodingVersion == 1 || encodingVersion == 2 || encodingVersion == 3 || encodingVersion == 4) { boolean hasBrightness = false; if (encodingVersion >= 3) { // 1 bit optional brightness inclusion flag hasBrightness = mBitSet.incrementingGet(); if (hasBrightness) // 8 bit optional global brightness { brightness = mBitSet.extractNumber(8); } } numChannels = mBitSet.extractNumber(6); moodBuilder.setNumChannels(numChannels); // 1 bit timing addressing reference mode. boolean isRelativeToMidnight = mBitSet.incrementingGet(); // 7 bit timing repeat number. Clients only support 0 (no loops) or 127 (infinite loops). int numLoops = mBitSet.extractNumber(7); boolean isLooping = (numLoops == 127); // 6 bit number of timestamps int numTimestamps = mBitSet.extractNumber(6); int[] timeArray = new int[numTimestamps]; for (int i = 0; i < numTimestamps; i++) { // 20 bit timestamp timeArray[i] = mBitSet.extractNumber(20); } boolean usesTiming = !(timeArray.length == 0 || (timeArray.length == 1 && timeArray[0] == 0)); if (usesTiming && isRelativeToMidnight) { moodBuilder.setTimingPolicy(Mood.TimingPolicy.DAILY); } else if (usesTiming && isLooping) { moodBuilder.setTimingPolicy(Mood.TimingPolicy.LOOPING); } else { moodBuilder.setTimingPolicy(Mood.TimingPolicy.BASIC); } moodBuilder.setUsesTiming(usesTiming); int numStates; if (encodingVersion >= 4) { // 12 bit number of states numStates = mBitSet.extractNumber(12); } else { // 6 bit number of states numStates = mBitSet.extractNumber(6); } BulbState[] stateArray = new BulbState[numStates]; for (int i = 0; i < numStates; i++) { // decode each state stateArray[i] = extractState(mBitSet); if (encodingVersion < 3) { // convert from old brightness stuffing to new relative brightness + total brightness // system if (stateArray[i].get255Bri() != null) { brightness = stateArray[i].get255Bri(); stateArray[i].set255Bri(null); } } } // number of events, 8 bits for encodings 1 & 2, 12 bits for 3+ int numEvents; if (encodingVersion <= 2) { numEvents = mBitSet.extractNumber(8); } else { numEvents = mBitSet.extractNumber(12); } Event[] eList = new Event[numEvents]; for (int i = 0; i < numEvents; i++) { int channel = mBitSet.extractNumber(getBitLength(numChannels)); long milliseconds = Utils.fromDeciSeconds(timeArray[mBitSet.extractNumber(getBitLength(numTimestamps))]); BulbState state = stateArray[mBitSet.extractNumber(getBitLength(numStates))]; eList[i] = new Event(state, channel, milliseconds); } moodBuilder.setEvents(eList); // 20 bit loopIterationTimeLength is only difference between encodingVersion=1 & =2 if (encodingVersion >= 2) { moodBuilder.setLoopMilliTime(Utils.fromDeciSeconds(mBitSet.extractNumber(20))); } } else if (encodingVersion == 0) { throw new UnsupportedEncodingException(); } else { throw new FutureEncodingException(); } Integer[] bulbs = null; if (hasBulbs) { bulbs = new Integer[bList.size()]; for (int i = 0; i < bList.size(); i++) { bulbs[i] = bList.get(i); } } Mood mood = moodBuilder.build(); return new Pair<Integer[], Pair<Mood, Integer>>(bulbs, new Pair<Mood, Integer>(mood, brightness)); } catch (FutureEncodingException e) { throw new FutureEncodingException(); } catch (Exception e) { throw new InvalidEncodingException(); } } }