Home | History | Annotate | Download | only in launcher3
      1 /*
      2  * Copyright (C) 2014 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 
     17 package com.android.launcher3;
     18 
     19 import android.appwidget.AppWidgetHost;
     20 import android.appwidget.AppWidgetManager;
     21 import android.content.ComponentName;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.pm.ActivityInfo;
     26 import android.content.pm.PackageManager;
     27 import android.content.res.Resources;
     28 import android.content.res.XmlResourceParser;
     29 import android.database.sqlite.SQLiteDatabase;
     30 import android.graphics.drawable.Drawable;
     31 import android.net.Uri;
     32 import android.os.Bundle;
     33 import android.text.TextUtils;
     34 import android.util.Log;
     35 import android.util.Pair;
     36 import android.util.Patterns;
     37 
     38 import com.android.launcher3.LauncherProvider.SqlArguments;
     39 import com.android.launcher3.LauncherProvider.WorkspaceLoader;
     40 import com.android.launcher3.LauncherSettings.Favorites;
     41 
     42 import org.xmlpull.v1.XmlPullParser;
     43 import org.xmlpull.v1.XmlPullParserException;
     44 
     45 import java.io.IOException;
     46 import java.util.ArrayList;
     47 import java.util.HashMap;
     48 
     49 /**
     50  * This class contains contains duplication of functionality as found in
     51  * LauncherProvider#DatabaseHelper. It has been isolated and differentiated in order
     52  * to cleanly and separately represent AutoInstall default layout format and policy.
     53  */
     54 public class AutoInstallsLayout implements WorkspaceLoader {
     55     private static final String TAG = "AutoInstalls";
     56     private static final boolean LOGD = true;
     57 
     58     /** Marker action used to discover a package which defines launcher customization */
     59     static final String ACTION_LAUNCHER_CUSTOMIZATION =
     60             "android.autoinstalls.config.action.PLAY_AUTO_INSTALL";
     61 
     62     private static final String LAYOUT_RES = "default_layout";
     63 
     64     static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost,
     65             LayoutParserCallback callback) {
     66         Pair<String, Resources> customizationApkInfo = Utilities.findSystemApk(
     67                 ACTION_LAUNCHER_CUSTOMIZATION, context.getPackageManager());
     68         if (customizationApkInfo == null) {
     69             return null;
     70         }
     71 
     72         String pkg = customizationApkInfo.first;
     73         Resources res = customizationApkInfo.second;
     74         int layoutId = res.getIdentifier(LAYOUT_RES, "xml", pkg);
     75         if (layoutId == 0) {
     76             Log.e(TAG, "Layout definition not found in package: " + pkg);
     77             return null;
     78         }
     79         return new AutoInstallsLayout(context, appWidgetHost, callback, pkg, res, layoutId);
     80     }
     81 
     82     // Object Tags
     83     private static final String TAG_WORKSPACE = "workspace";
     84     private static final String TAG_APP_ICON = "appicon";
     85     private static final String TAG_AUTO_INSTALL = "autoinstall";
     86     private static final String TAG_FOLDER = "folder";
     87     private static final String TAG_APPWIDGET = "appwidget";
     88     private static final String TAG_SHORTCUT = "shortcut";
     89     private static final String TAG_EXTRA = "extra";
     90 
     91     private static final String ATTR_CONTAINER = "container";
     92     private static final String ATTR_RANK = "rank";
     93 
     94     private static final String ATTR_PACKAGE_NAME = "packageName";
     95     private static final String ATTR_CLASS_NAME = "className";
     96     private static final String ATTR_TITLE = "title";
     97     private static final String ATTR_SCREEN = "screen";
     98     private static final String ATTR_X = "x";
     99     private static final String ATTR_Y = "y";
    100     private static final String ATTR_SPAN_X = "spanX";
    101     private static final String ATTR_SPAN_Y = "spanY";
    102     private static final String ATTR_ICON = "icon";
    103     private static final String ATTR_URL = "url";
    104 
    105     // Style attrs -- "Extra"
    106     private static final String ATTR_KEY = "key";
    107     private static final String ATTR_VALUE = "value";
    108 
    109     private static final String HOTSEAT_CONTAINER_NAME =
    110             Favorites.containerToString(Favorites.CONTAINER_HOTSEAT);
    111 
    112     private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE =
    113             "com.android.launcher.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE";
    114 
    115     private final Context mContext;
    116     private final AppWidgetHost mAppWidgetHost;
    117     private final LayoutParserCallback mCallback;
    118 
    119     private final PackageManager mPackageManager;
    120     private final ContentValues mValues;
    121 
    122     private final Resources mRes;
    123     private final int mLayoutId;
    124 
    125     private SQLiteDatabase mDb;
    126 
    127     public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost,
    128             LayoutParserCallback callback, String packageName, Resources res, int layoutId) {
    129         mContext = context;
    130         mAppWidgetHost = appWidgetHost;
    131         mCallback = callback;
    132 
    133         mPackageManager = context.getPackageManager();
    134         mValues = new ContentValues();
    135 
    136         mRes = res;
    137         mLayoutId = layoutId;
    138     }
    139 
    140     @Override
    141     public int loadLayout(SQLiteDatabase db, ArrayList<Long> screenIds) {
    142         mDb = db;
    143         try {
    144             return parseLayout(mRes, mLayoutId, screenIds);
    145         } catch (XmlPullParserException | IOException | RuntimeException e) {
    146             Log.w(TAG, "Got exception parsing layout.", e);
    147             return -1;
    148         }
    149     }
    150 
    151     private int parseLayout(Resources res, int layoutId, ArrayList<Long> screenIds)
    152             throws XmlPullParserException, IOException {
    153         final int hotseatAllAppsRank = LauncherAppState.getInstance()
    154                 .getDynamicGrid().getDeviceProfile().hotseatAllAppsRank;
    155 
    156         XmlResourceParser parser = res.getXml(layoutId);
    157         beginDocument(parser, TAG_WORKSPACE);
    158         final int depth = parser.getDepth();
    159         int type;
    160         HashMap<String, TagParser> tagParserMap = getLayoutElementsMap();
    161         int count = 0;
    162 
    163         while (((type = parser.next()) != XmlPullParser.END_TAG ||
    164                 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
    165             if (type != XmlPullParser.START_TAG) {
    166                 continue;
    167             }
    168 
    169             mValues.clear();
    170             final int container;
    171             final long screenId;
    172 
    173             if (HOTSEAT_CONTAINER_NAME.equals(getAttributeValue(parser, ATTR_CONTAINER))) {
    174                 container = Favorites.CONTAINER_HOTSEAT;
    175 
    176                 // Hack: hotseat items are stored using screen ids
    177                 long rank = Long.parseLong(getAttributeValue(parser, ATTR_RANK));
    178                 screenId = (rank < hotseatAllAppsRank) ? rank : (rank + 1);
    179 
    180             } else {
    181                 container = Favorites.CONTAINER_DESKTOP;
    182                 screenId = Long.parseLong(getAttributeValue(parser, ATTR_SCREEN));
    183 
    184                 mValues.put(Favorites.CELLX, getAttributeValue(parser, ATTR_X));
    185                 mValues.put(Favorites.CELLY, getAttributeValue(parser, ATTR_Y));
    186             }
    187 
    188             mValues.put(Favorites.CONTAINER, container);
    189             mValues.put(Favorites.SCREEN, screenId);
    190 
    191             TagParser tagParser = tagParserMap.get(parser.getName());
    192             if (tagParser == null) {
    193                 if (LOGD) Log.d(TAG, "Ignoring unknown element tag: " + parser.getName());
    194                 continue;
    195             }
    196             long newElementId = tagParser.parseAndAdd(parser, res);
    197             if (newElementId >= 0) {
    198                 // Keep track of the set of screens which need to be added to the db.
    199                 if (!screenIds.contains(screenId) &&
    200                         container == Favorites.CONTAINER_DESKTOP) {
    201                     screenIds.add(screenId);
    202                 }
    203                 count++;
    204             }
    205         }
    206         return count;
    207     }
    208 
    209     protected long addShortcut(String title, Intent intent, int type) {
    210         long id = mCallback.generateNewItemId();
    211         mValues.put(Favorites.INTENT, intent.toUri(0));
    212         mValues.put(Favorites.TITLE, title);
    213         mValues.put(Favorites.ITEM_TYPE, type);
    214         mValues.put(Favorites.SPANX, 1);
    215         mValues.put(Favorites.SPANY, 1);
    216         mValues.put(Favorites._ID, id);
    217         if (mCallback.insertAndCheck(mDb, mValues) < 0) {
    218             return -1;
    219         } else {
    220             return id;
    221         }
    222     }
    223 
    224     protected HashMap<String, TagParser> getFolderElementsMap() {
    225         HashMap<String, TagParser> parsers = new HashMap<String, TagParser>();
    226         parsers.put(TAG_APP_ICON, new AppShortcutParser());
    227         parsers.put(TAG_AUTO_INSTALL, new AutoInstallParser());
    228         parsers.put(TAG_SHORTCUT, new ShortcutParser());
    229         return parsers;
    230     }
    231 
    232     protected HashMap<String, TagParser> getLayoutElementsMap() {
    233         HashMap<String, TagParser> parsers = new HashMap<String, TagParser>();
    234         parsers.put(TAG_APP_ICON, new AppShortcutParser());
    235         parsers.put(TAG_AUTO_INSTALL, new AutoInstallParser());
    236         parsers.put(TAG_FOLDER, new FolderParser());
    237         parsers.put(TAG_APPWIDGET, new AppWidgetParser());
    238         parsers.put(TAG_SHORTCUT, new ShortcutParser());
    239         return parsers;
    240     }
    241 
    242     private interface TagParser {
    243         /**
    244          * Parses the tag and adds to the db
    245          * @return the id of the row added or -1;
    246          */
    247         long parseAndAdd(XmlResourceParser parser, Resources res)
    248                 throws XmlPullParserException, IOException;
    249     }
    250 
    251     private class AppShortcutParser implements TagParser {
    252 
    253         @Override
    254         public long parseAndAdd(XmlResourceParser parser, Resources res) {
    255             final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
    256             final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
    257 
    258             if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(className)) {
    259                 ActivityInfo info;
    260                 try {
    261                     ComponentName cn;
    262                     try {
    263                         cn = new ComponentName(packageName, className);
    264                         info = mPackageManager.getActivityInfo(cn, 0);
    265                     } catch (PackageManager.NameNotFoundException nnfe) {
    266                         String[] packages = mPackageManager.currentToCanonicalPackageNames(
    267                                 new String[] { packageName });
    268                         cn = new ComponentName(packages[0], className);
    269                         info = mPackageManager.getActivityInfo(cn, 0);
    270                     }
    271                     final Intent intent = new Intent(Intent.ACTION_MAIN, null)
    272                         .addCategory(Intent.CATEGORY_LAUNCHER)
    273                         .setComponent(cn)
    274                         .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
    275                                 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
    276 
    277                     return addShortcut(info.loadLabel(mPackageManager).toString(),
    278                             intent, Favorites.ITEM_TYPE_APPLICATION);
    279                 } catch (PackageManager.NameNotFoundException e) {
    280                     Log.w(TAG, "Unable to add favorite: " + packageName + "/" + className, e);
    281                 }
    282                 return -1;
    283             } else {
    284                 if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component or uri");
    285                 return -1;
    286             }
    287         }
    288     }
    289 
    290     private class AutoInstallParser implements TagParser {
    291 
    292         @Override
    293         public long parseAndAdd(XmlResourceParser parser, Resources res) {
    294             final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
    295             final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
    296             if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) {
    297                 if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component");
    298                 return -1;
    299             }
    300 
    301             mValues.put(Favorites.RESTORED, ShortcutInfo.FLAG_AUTOINTALL_ICON);
    302             final Intent intent = new Intent(Intent.ACTION_MAIN, null)
    303                 .addCategory(Intent.CATEGORY_LAUNCHER)
    304                 .setComponent(new ComponentName(packageName, className))
    305                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
    306                         Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
    307             return addShortcut(mContext.getString(R.string.package_state_unknown), intent,
    308                     Favorites.ITEM_TYPE_APPLICATION);
    309         }
    310     }
    311 
    312     private class ShortcutParser implements TagParser {
    313 
    314         @Override
    315         public long parseAndAdd(XmlResourceParser parser, Resources res) {
    316             final String url = getAttributeValue(parser, ATTR_URL);
    317             final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0);
    318             final int iconId = getAttributeResourceValue(parser, ATTR_ICON, 0);
    319 
    320             if (titleResId == 0 || iconId == 0) {
    321                 if (LOGD) Log.d(TAG, "Ignoring shortcut");
    322                 return -1;
    323             }
    324 
    325             if (TextUtils.isEmpty(url) || !Patterns.WEB_URL.matcher(url).matches()) {
    326                 if (LOGD) Log.d(TAG, "Ignoring shortcut, invalid url: " + url);
    327                 return -1;
    328             }
    329             Drawable icon = res.getDrawable(iconId);
    330             if (icon == null) {
    331                 if (LOGD) Log.d(TAG, "Ignoring shortcut, can't load icon");
    332                 return -1;
    333             }
    334 
    335             ItemInfo.writeBitmap(mValues, Utilities.createIconBitmap(icon, mContext));
    336             final Intent intent = new Intent(Intent.ACTION_VIEW, null)
    337                 .setData(Uri.parse(url))
    338                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
    339                         Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
    340             return addShortcut(res.getString(titleResId), intent, Favorites.ITEM_TYPE_SHORTCUT);
    341         }
    342     }
    343 
    344     private class AppWidgetParser implements TagParser {
    345 
    346         @Override
    347         public long parseAndAdd(XmlResourceParser parser, Resources res)
    348                 throws XmlPullParserException, IOException {
    349             final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
    350             final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
    351             if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) {
    352                 if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component");
    353                 return -1;
    354             }
    355 
    356             ComponentName cn = new ComponentName(packageName, className);
    357             try {
    358                 mPackageManager.getReceiverInfo(cn, 0);
    359             } catch (Exception e) {
    360                 String[] packages = mPackageManager.currentToCanonicalPackageNames(
    361                         new String[] { packageName });
    362                 cn = new ComponentName(packages[0], className);
    363                 try {
    364                     mPackageManager.getReceiverInfo(cn, 0);
    365                 } catch (Exception e1) {
    366                     if (LOGD) Log.d(TAG, "Can't find widget provider: " + className);
    367                     return -1;
    368                 }
    369             }
    370 
    371             mValues.put(Favorites.SPANX, getAttributeValue(parser, ATTR_SPAN_X));
    372             mValues.put(Favorites.SPANY, getAttributeValue(parser, ATTR_SPAN_Y));
    373 
    374             // Read the extras
    375             Bundle extras = new Bundle();
    376             int widgetDepth = parser.getDepth();
    377             int type;
    378             while ((type = parser.next()) != XmlPullParser.END_TAG ||
    379                     parser.getDepth() > widgetDepth) {
    380                 if (type != XmlPullParser.START_TAG) {
    381                     continue;
    382                 }
    383 
    384                 if (TAG_EXTRA.equals(parser.getName())) {
    385                     String key = getAttributeValue(parser, ATTR_KEY);
    386                     String value = getAttributeValue(parser, ATTR_VALUE);
    387                     if (key != null && value != null) {
    388                         extras.putString(key, value);
    389                     } else {
    390                         throw new RuntimeException("Widget extras must have a key and value");
    391                     }
    392                 } else {
    393                     throw new RuntimeException("Widgets can contain only extras");
    394                 }
    395             }
    396 
    397             final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
    398             long insertedId = -1;
    399             try {
    400                 int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
    401 
    402                 if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, cn)) {
    403                     if (LOGD) Log.e(TAG, "Unable to bind app widget id " + cn);
    404                     return -1;
    405                 }
    406 
    407                 mValues.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
    408                 mValues.put(Favorites.APPWIDGET_ID, appWidgetId);
    409                 mValues.put(Favorites.APPWIDGET_PROVIDER, cn.flattenToString());
    410                 mValues.put(Favorites._ID, mCallback.generateNewItemId());
    411                 insertedId = mCallback.insertAndCheck(mDb, mValues);
    412                 if (insertedId < 0) {
    413                     mAppWidgetHost.deleteAppWidgetId(appWidgetId);
    414                     return insertedId;
    415                 }
    416 
    417                 // Send a broadcast to configure the widget
    418                 if (!extras.isEmpty()) {
    419                     Intent intent = new Intent(ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE);
    420                     intent.setComponent(cn);
    421                     intent.putExtras(extras);
    422                     intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
    423                     mContext.sendBroadcast(intent);
    424                 }
    425             } catch (RuntimeException ex) {
    426                 if (LOGD) Log.e(TAG, "Problem allocating appWidgetId", ex);
    427             }
    428             return insertedId;
    429         }
    430     }
    431 
    432     private class FolderParser implements TagParser {
    433         private final HashMap<String, TagParser> mFolderElements = getFolderElementsMap();
    434 
    435         @Override
    436         public long parseAndAdd(XmlResourceParser parser, Resources res)
    437                 throws XmlPullParserException, IOException {
    438             final String title;
    439             final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0);
    440             if (titleResId != 0) {
    441                 title = res.getString(titleResId);
    442             } else {
    443                 title = mContext.getResources().getString(R.string.folder_name);
    444             }
    445 
    446             mValues.put(Favorites.TITLE, title);
    447             mValues.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_FOLDER);
    448             mValues.put(Favorites.SPANX, 1);
    449             mValues.put(Favorites.SPANY, 1);
    450             mValues.put(Favorites._ID, mCallback.generateNewItemId());
    451             long folderId = mCallback.insertAndCheck(mDb, mValues);
    452             if (folderId < 0) {
    453                 if (LOGD) Log.e(TAG, "Unable to add folder");
    454                 return -1;
    455             }
    456 
    457             final ContentValues myValues = new ContentValues(mValues);
    458             ArrayList<Long> folderItems = new ArrayList<Long>();
    459 
    460             int type;
    461             int folderDepth = parser.getDepth();
    462             while ((type = parser.next()) != XmlPullParser.END_TAG ||
    463                     parser.getDepth() > folderDepth) {
    464                 if (type != XmlPullParser.START_TAG) {
    465                     continue;
    466                 }
    467                 mValues.clear();
    468                 mValues.put(Favorites.CONTAINER, folderId);
    469 
    470                 TagParser tagParser = mFolderElements.get(parser.getName());
    471                 if (tagParser != null) {
    472                     final long id = tagParser.parseAndAdd(parser, res);
    473                     if (id >= 0) {
    474                         folderItems.add(id);
    475                     }
    476                 } else {
    477                     throw new RuntimeException("Invalid folder item " + parser.getName());
    478                 }
    479             }
    480 
    481             long addedId = folderId;
    482 
    483             // We can only have folders with >= 2 items, so we need to remove the
    484             // folder and clean up if less than 2 items were included, or some
    485             // failed to add, and less than 2 were actually added
    486             if (folderItems.size() < 2) {
    487                 // Delete the folder
    488                 Uri uri = Favorites.getContentUri(folderId, false);
    489                 SqlArguments args = new SqlArguments(uri, null, null);
    490                 mDb.delete(args.table, args.where, args.args);
    491                 addedId = -1;
    492 
    493                 // If we have a single item, promote it to where the folder
    494                 // would have been.
    495                 if (folderItems.size() == 1) {
    496                     final ContentValues childValues = new ContentValues();
    497                     copyInteger(myValues, childValues, Favorites.CONTAINER);
    498                     copyInteger(myValues, childValues, Favorites.SCREEN);
    499                     copyInteger(myValues, childValues, Favorites.CELLX);
    500                     copyInteger(myValues, childValues, Favorites.CELLY);
    501 
    502                     addedId = folderItems.get(0);
    503                     mDb.update(LauncherProvider.TABLE_FAVORITES, childValues,
    504                             Favorites._ID + "=" + addedId, null);
    505                 }
    506             }
    507             return addedId;
    508         }
    509     }
    510 
    511     private static final void beginDocument(XmlPullParser parser, String firstElementName)
    512             throws XmlPullParserException, IOException {
    513         int type;
    514         while ((type = parser.next()) != XmlPullParser.START_TAG
    515                 && type != XmlPullParser.END_DOCUMENT);
    516 
    517         if (type != XmlPullParser.START_TAG) {
    518             throw new XmlPullParserException("No start tag found");
    519         }
    520 
    521         if (!parser.getName().equals(firstElementName)) {
    522             throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() +
    523                     ", expected " + firstElementName);
    524         }
    525     }
    526 
    527     /**
    528      * Return attribute value, attempting launcher-specific namespace first
    529      * before falling back to anonymous attribute.
    530      */
    531     private static String getAttributeValue(XmlResourceParser parser, String attribute) {
    532         String value = parser.getAttributeValue(
    533                 "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute);
    534         if (value == null) {
    535             value = parser.getAttributeValue(null, attribute);
    536         }
    537         return value;
    538     }
    539 
    540     /**
    541      * Return attribute resource value, attempting launcher-specific namespace
    542      * first before falling back to anonymous attribute.
    543      */
    544     private static int getAttributeResourceValue(XmlResourceParser parser, String attribute,
    545             int defaultValue) {
    546         int value = parser.getAttributeResourceValue(
    547                 "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute,
    548                 defaultValue);
    549         if (value == defaultValue) {
    550             value = parser.getAttributeResourceValue(null, attribute, defaultValue);
    551         }
    552         return value;
    553     }
    554 
    555     public static interface LayoutParserCallback {
    556         long generateNewItemId();
    557 
    558         long insertAndCheck(SQLiteDatabase db, ContentValues values);
    559     }
    560 
    561     private static void copyInteger(ContentValues from, ContentValues to, String key) {
    562         to.put(key, from.getAsInteger(key));
    563     }
    564 }
    565