Home | History | Annotate | Download | only in launcher3
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.launcher3;
     17 
     18 import com.android.launcher3.backup.BackupProtos.CheckedMessage;
     19 import com.android.launcher3.backup.BackupProtos.Favorite;
     20 import com.android.launcher3.backup.BackupProtos.Key;
     21 import com.android.launcher3.backup.BackupProtos.Journal;
     22 import com.android.launcher3.backup.BackupProtos.Resource;
     23 import com.android.launcher3.backup.BackupProtos.Screen;
     24 import com.android.launcher3.backup.BackupProtos.Widget;
     25 
     26 import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
     27 import com.google.protobuf.nano.MessageNano;
     28 
     29 import java.io.BufferedInputStream;
     30 import java.io.ByteArrayOutputStream;
     31 import java.io.File;
     32 import java.io.FileInputStream;
     33 import java.io.FileNotFoundException;
     34 import java.io.FileOutputStream;
     35 import java.io.IOException;
     36 import java.lang.System;
     37 import java.util.LinkedList;
     38 import java.util.List;
     39 import java.util.zip.CRC32;
     40 
     41 import javax.xml.bind.DatatypeConverter;
     42 
     43 
     44 /**
     45  * Commandline utility for decoding Launcher3 backup protocol buffers.
     46  *
     47  * <P>When using com.android.internal.backup.LocalTransport, the file names are base64-encoded Key
     48  * protocol buffers with a prefix, that have been base64-encoded again by the transport:
     49  * <pre>
     50  *     echo "TDpDQUlnL0pxVTVnOD0=" | launcher_protoutil -k
     51  * </pre>
     52  *
     53  * <P>This tool understands these file names and will use the embedded Key to detect the type and
     54  * extract the payload automatically:
     55  * <pre>
     56  *     launcher_protoutil /tmp/TDpDQUlnL0pxVTVnOD0=
     57  * </pre>
     58  *
     59  * <P>With payload debugging enabled, base64-encoded protocol buffers will be written to the logs.
     60  * Copy the encoded snippet from the log, and specify the type explicitly, with the Logs flags:
     61  * <pre>
     62  *    echo "CAEYLiCJ9JKsDw==" | launcher_protoutil -L -k
     63  * </pre>
     64  * For backup payloads it is more convenient to copy the log snippet to a file:
     65  * <pre>
     66  *    launcher_protoutil -L -f favorite.log
     67  * </pre>
     68  */
     69 class DecoderRing {
     70 
     71     public static final String STANDARD_IN = "**stdin**";
     72 
     73     private static Class[] TYPES = {
     74             Key.class,
     75             Favorite.class,
     76             Screen.class,
     77             Resource.class,
     78             Widget.class
     79     };
     80     static final int ICON_TYPE_BITMAP = 1;
     81 
     82     public static void main(String[ ] args)
     83             throws Exception {
     84         Class defaultType = null;
     85         boolean extractImages = false;
     86         boolean fromLogs = false;
     87         int skip = 0;
     88         List<File> files = new LinkedList<File>();
     89         boolean verbose = false;
     90 
     91         for (int i = 0; i < args.length; i++) {
     92             if ("-k".equals(args[i])) {
     93                 defaultType = Key.class;
     94             } else if ("-f".equals(args[i])) {
     95                 defaultType = Favorite.class;
     96             } else if ("-j".equals(args[i])) {
     97                 defaultType = Journal.class;
     98             } else if ("-i".equals(args[i])) {
     99                 defaultType = Resource.class;
    100             } else if ("-s".equals(args[i])) {
    101                 defaultType = Screen.class;
    102             } else if ("-w".equals(args[i])) {
    103                 defaultType = Widget.class;
    104             } else if ("-S".equals(args[i])) {
    105                 if ((i + 1) < args.length) {
    106                     skip = Integer.valueOf(args[++i]);
    107                 } else {
    108                     usage(args);
    109                 }
    110             } else if ("-x".equals(args[i])) {
    111                 extractImages = true;
    112             } else if ("-v".equals(args[i])) {
    113                 verbose = true;
    114             } else if ("-L".equals(args[i])) {
    115                 fromLogs = true;
    116             } else if (args[i] != null && !args[i].startsWith("-")) {
    117                 files.add(new File(args[i]));
    118             } else {
    119                 System.err.println("Unsupported flag: " + args[i]);
    120                 usage(args);
    121             }
    122         }
    123 
    124         if (defaultType == null && files.isEmpty()) {
    125             // can't infer file type without the key
    126             usage(args);
    127         }
    128 
    129         if (files.size() > 1 && defaultType != null) {
    130             System.err.println("Explicit type ignored for multiple files.");
    131             defaultType = null;
    132         }
    133 
    134         if (files.isEmpty()) {
    135             files.add(new File(STANDARD_IN));
    136         }
    137 
    138         for (File source : files) {
    139             Class type = null;
    140             if (defaultType == null) {
    141                 Key key = decodeKey(source.getName().getBytes(), fromLogs);
    142                 if (key != null) {
    143                     type = TYPES[key.type];
    144                     if (verbose) {
    145                         System.err.println(source.getName() + " is a " + type.getSimpleName());
    146                         System.out.println(key.toString());
    147                     }
    148                 }
    149             } else {
    150                 type = defaultType;
    151             }
    152 
    153             // read in the bytes
    154             ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
    155             BufferedInputStream input = null;
    156             if (source.getName() == STANDARD_IN) {
    157                 input = new BufferedInputStream(System.in);
    158             } else {
    159                 try {
    160                     input = new BufferedInputStream(new FileInputStream(source));
    161                 } catch (FileNotFoundException e) {
    162                     System.err.println("failed to open file: " + source + ", " + e);
    163                     System.exit(1);
    164                 }
    165             }
    166             byte[] buffer = new byte[1024];
    167             try {
    168                 while (input.available() > 0) {
    169                     int n = input.read(buffer);
    170                     int offset = 0;
    171                     if (skip > 0) {
    172                         offset = Math.min(skip, n);
    173                         n -= offset;
    174                         skip -= offset;
    175                     }
    176                     if (n > 0) {
    177                         byteStream.write(buffer, offset, n);
    178                     }
    179                 }
    180             } catch (IOException e) {
    181                 System.err.println("failed to read input: " + e);
    182                 System.exit(1);
    183             }
    184 
    185             MessageNano proto = null;
    186             byte[] payload = byteStream.toByteArray();
    187             if (type == Key.class) {
    188                 proto = decodeKey(payload, fromLogs);
    189             } else if (type != null) {
    190                 proto = decodeBackupData(payload, type, fromLogs);
    191             }
    192 
    193             // Generic string output
    194             if (proto != null) {
    195                 System.out.println(proto.toString());
    196             }
    197 
    198             if (extractImages) {
    199                 String prefix = "stdin";
    200                 if (source != null) {
    201                     prefix = source.getName();
    202                 }
    203                 // save off the icon bits in a file for inspection
    204                 if (proto instanceof Resource) {
    205                     Resource icon = (Resource) proto;
    206                     writeImageData(icon.data, prefix + ".png");
    207                 }
    208 
    209                 // save off the icon bits in a file for inspection
    210                 if (proto instanceof Favorite) {
    211                     Favorite favorite = (Favorite) proto;
    212                     if (favorite.iconType == ICON_TYPE_BITMAP) {
    213                         writeImageData(favorite.icon, prefix + ".png");
    214                     }
    215                 }
    216 
    217                 // save off the widget icon and preview bits in files for inspection
    218                 if (proto instanceof Widget) {
    219                     Widget widget = (Widget) proto;
    220                     if (widget.icon != null) {
    221                         writeImageData(widget.icon.data, prefix + "_icon.png");
    222                     }
    223                     if (widget.preview != null) {
    224                         writeImageData(widget.preview.data, prefix + "_preview.png");
    225                     }
    226                 }
    227             }
    228         }
    229         System.exit(0);
    230     }
    231 
    232     // In logcat, backup data is base64 encoded, but in localtransport files it is raw
    233     private static MessageNano decodeBackupData(byte[] payload, Class type, boolean fromLogs)
    234             throws InstantiationException, IllegalAccessException {
    235         MessageNano proto;// other types are wrapped in a checksum message
    236         CheckedMessage wrapper = new CheckedMessage();
    237         try {
    238             if (fromLogs) {
    239                 payload = DatatypeConverter.parseBase64Binary(new String(payload));
    240             }
    241             MessageNano.mergeFrom(wrapper, payload);
    242         } catch (InvalidProtocolBufferNanoException e) {
    243             System.err.println("failed to parse wrapper: " + e);
    244             System.exit(1);
    245         }
    246 
    247         CRC32 checksum = new CRC32();
    248         checksum.update(wrapper.payload);
    249         if (wrapper.checksum != checksum.getValue()) {
    250             System.err.println("wrapper checksum failed");
    251             System.exit(1);
    252         }
    253 
    254         // decode the actual message
    255         proto = (MessageNano) type.newInstance();
    256         try {
    257             MessageNano.mergeFrom(proto, wrapper.payload);
    258         } catch (InvalidProtocolBufferNanoException e) {
    259             System.err.println("failed to parse proto: " + e);
    260             System.exit(1);
    261         }
    262         return proto;
    263     }
    264 
    265     // In logcat, keys are base64 encoded with no prefix.
    266     // The localtransport adds a prefix and the base64 encodes the whole thing again.
    267     private static Key decodeKey(byte[] payload, boolean fromLogs) {
    268         Key key = new Key();
    269         try {
    270             String encodedKey = new String(payload);
    271             if (!fromLogs) {
    272                 byte[] rawKey = DatatypeConverter.parseBase64Binary(encodedKey);
    273                 if (rawKey[0] != 'L' || rawKey[1] != ':') {
    274                     System.err.println(encodedKey + " is not a launcher backup key.");
    275                     return null;
    276                 }
    277                 encodedKey = new String(rawKey, 2, rawKey.length - 2);
    278             }
    279             byte[] keyProtoData = DatatypeConverter.parseBase64Binary(encodedKey);
    280             key = Key.parseFrom(keyProtoData);
    281         } catch (InvalidProtocolBufferNanoException protoException) {
    282             System.err.println("failed to extract key from filename: " + protoException);
    283             return null;
    284         } catch (IllegalArgumentException base64Exception) {
    285             System.err.println("failed to extract key from filename: " + base64Exception);
    286             return null;
    287         }
    288 
    289         // keys are self-checked
    290         if (key.checksum != checkKey(key)) {
    291             System.err.println("key ckecksum failed");
    292             return null;
    293         }
    294         return key;
    295     }
    296 
    297     private static void writeImageData(byte[] data, String path) {
    298         FileOutputStream iconFile = null;
    299         try {
    300             iconFile = new FileOutputStream(path);
    301             iconFile.write(data);
    302             System.err.println("wrote " + path);
    303         } catch (IOException e) {
    304             System.err.println("failed to write image file: " + e);
    305         } finally {
    306             if (iconFile != null) {
    307                 try {
    308                     iconFile.close();
    309                 } catch (IOException e) {
    310                     System.err.println("failed to close the image file: " + e);
    311                 }
    312             }
    313         }
    314     }
    315 
    316     private static long checkKey(Key key) {
    317         CRC32 checksum = new CRC32();
    318         checksum.update(key.type);
    319         checksum.update((int) (key.id & 0xffff));
    320         checksum.update((int) ((key.id >> 32) & 0xffff));
    321         if (key.name != null && key.name.length() > 0) {
    322             checksum.update(key.name.getBytes());
    323         }
    324         return checksum.getValue();
    325     }
    326 
    327     private static void usage(String[] args) {
    328         System.err.println("launcher_protoutil [-x] [-S b] [-k|-f|-i|-s|-w] [filename]");
    329         System.err.println("\t-k\tdecode a key");
    330         System.err.println("\t-f\tdecode a favorite");
    331         System.err.println("\t-i\tdecode a icon");
    332         System.err.println("\t-s\tdecode a screen");
    333         System.err.println("\t-w\tdecode a widget");
    334         System.err.println("\t-S b\tskip b bytes");
    335         System.err.println("\t-x\textract image data to files");
    336         System.err.println("\t-v\tprint key type data, as well as payload");
    337         System.err.println("\t-l\texpect data from logcat, instead of the local transport");
    338         System.err.println("\tfilename\tread from filename, not stdin");
    339         System.exit(1);
    340     }
    341 }