1ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian/*
2ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian * Copyright (C) 2008 The Android Open Source Project
3ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian *
4ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian * Licensed under the Apache License, Version 2.0 (the "License");
5ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian * you may not use this file except in compliance with the License.
6ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian * You may obtain a copy of the License at
7ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian *
8ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian *      http://www.apache.org/licenses/LICENSE-2.0
9ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian *
10ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian * Unless required by applicable law or agreed to in writing, software
11ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian * distributed under the License is distributed on an "AS IS" BASIS,
12ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian * See the License for the specific language governing permissions and
14ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian * limitations under the License.
15ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian */
16ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
17ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanianpackage com.android.incallui;
18ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
19ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanianimport android.app.Notification;
20ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanianimport android.content.Context;
21ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanianimport android.graphics.Bitmap;
22ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanianimport android.graphics.drawable.BitmapDrawable;
23ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanianimport android.graphics.drawable.Drawable;
24ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanianimport android.net.Uri;
25ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanianimport android.support.annotation.MainThread;
26fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianimport android.support.annotation.Nullable;
27ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanianimport android.support.annotation.WorkerThread;
28fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianimport com.android.dialer.common.LogUtil;
29fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianimport com.android.dialer.common.concurrent.DialerExecutor;
300cd36a6e3bfda1c8b78c0375225929f50ee408a5zachhimport com.android.dialer.common.concurrent.DialerExecutorComponent;
31ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanianimport java.io.IOException;
32ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanianimport java.io.InputStream;
33ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
34ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian/** Helper class for loading contacts photo asynchronously. */
35ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanianpublic class ContactsAsyncHelper {
36ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
37ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian  /** Interface for a WorkerHandler result return. */
38fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  interface OnImageLoadCompleteListener {
39ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
40ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    /**
41ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     * Called when the image load is complete. Must be called in main thread.
42ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     *
43ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context,
44ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     *     Uri, OnImageLoadCompleteListener, Object)}.
45ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     * @param photo Drawable object obtained by the async load.
46ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     * @param photoIcon Bitmap object obtained by the async load.
47ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context,
48ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     *     Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original cookie is null.
49ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     */
50ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    @MainThread
51ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie);
52ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
53ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    /** Called when image is loaded to udpate data. Must be called in worker thread. */
54ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    @WorkerThread
55ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie);
56ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian  }
57ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
58ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian  /**
59ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   * Starts an asynchronous image load. After finishing the load, {@link
60ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   * OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} will be called.
61ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   *
62ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   * @param token Arbitrary integer which will be returned as the first argument of {@link
63ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   *     OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
64ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   * @param context Context object used to do the time-consuming operation.
65ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   * @param displayPhotoUri Uri to be used to fetch the photo
66ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   * @param listener Callback object which will be used when the asynchronous load is done. Can be
67ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   *     null, which means only the asynchronous load is done while there's no way to obtain the
68ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   *     loaded photos.
69ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   * @param cookie Arbitrary object the caller wants to remember, which will become the fourth
70ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   *     argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap,
71ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   *     Object)}. Can be null, at which the callback will also has null for the argument.
72ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian   */
73fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  static void startObtainPhotoAsync(
74ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      int token,
75ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      Context context,
76ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      Uri displayPhotoUri,
77ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      OnImageLoadCompleteListener listener,
78ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      Object cookie) {
79ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    // in case the source caller info is null, the URI will be null as well.
80ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    // just update using the placeholder image in this case.
81ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    if (displayPhotoUri == null) {
82fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian      LogUtil.e("ContactsAsyncHelper.startObjectPhotoAsync", "uri is missing");
83ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      return;
84ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    }
85ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
86ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    // Added additional Cookie field in the callee to handle arguments
87ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    // sent to the callback function.
88ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
89ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    // setup arguments
90ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    WorkerArgs args = new WorkerArgs();
91fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    args.token = token;
92ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    args.cookie = cookie;
93ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    args.context = context;
94ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    args.displayPhotoUri = displayPhotoUri;
95ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    args.listener = listener;
96ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
970cd36a6e3bfda1c8b78c0375225929f50ee408a5zachh    DialerExecutorComponent.get(context)
980cd36a6e3bfda1c8b78c0375225929f50ee408a5zachh        .dialerExecutorFactory()
990cd36a6e3bfda1c8b78c0375225929f50ee408a5zachh        .createNonUiTaskBuilder(new Worker())
100fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        .onSuccess(
101fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian            output -> {
102fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian              if (args.listener != null) {
103fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                LogUtil.d(
104fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                    "ContactsAsyncHelper.startObtainPhotoAsync",
105fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                    "notifying listener: "
106fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                        + args.listener
107fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                        + " image: "
108fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                        + args.displayPhotoUri
109fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                        + " completed");
110fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                args.listener.onImageLoadComplete(
111fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                    args.token, args.photo, args.photoIcon, args.cookie);
112fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian              }
113fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian            })
114fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        .build()
115fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        .executeParallel(args);
116ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian  }
117ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
118ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian  private static final class WorkerArgs {
119ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
120fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    public int token;
121ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    public Context context;
122ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    public Uri displayPhotoUri;
123ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    public Drawable photo;
124ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    public Bitmap photoIcon;
125ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    public Object cookie;
126ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    public OnImageLoadCompleteListener listener;
127ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian  }
128ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
129fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  private static class Worker implements DialerExecutor.Worker<WorkerArgs, Void> {
130ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
131fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    @Nullable
132ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    @Override
133fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    public Void doInBackground(WorkerArgs args) throws Throwable {
134fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian      InputStream inputStream = null;
135fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian      try {
136fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        try {
137fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          inputStream = args.context.getContentResolver().openInputStream(args.displayPhotoUri);
138fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        } catch (Exception e) {
139fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          LogUtil.e(
140fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian              "ContactsAsyncHelper.Worker.doInBackground", "error opening photo input stream", e);
141fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        }
142ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
143fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        if (inputStream != null) {
144fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          args.photo = Drawable.createFromStream(inputStream, args.displayPhotoUri.toString());
145fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
146fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          // This assumes Drawable coming from contact database is usually
147fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          // BitmapDrawable and thus we can have (down)scaled version of it.
148fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
149fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
150fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          LogUtil.d(
151fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian              "ContactsAsyncHelper.Worker.doInBackground",
152fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian              "loading image, URI: %s",
153fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian              args.displayPhotoUri);
154fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        } else {
155fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          args.photo = null;
156fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          args.photoIcon = null;
157fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          LogUtil.d(
158fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian              "ContactsAsyncHelper.Worker.doInBackground",
159fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian              "problem with image, URI: %s, using default image.",
160fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian              args.displayPhotoUri);
161fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        }
162fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        if (args.listener != null) {
163fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          args.listener.onImageLoaded(args.token, args.photo, args.photoIcon, args.cookie);
164fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        }
165fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian      } finally {
166fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        if (inputStream != null) {
167ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian          try {
168fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian            inputStream.close();
169fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          } catch (IOException e) {
170fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian            LogUtil.e(
171fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                "ContactsAsyncHelper.Worker.doInBackground", "Unable to close input stream.", e);
172ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian          }
173fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        }
174ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      }
175fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian      return null;
176ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    }
177ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
178ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    /**
179ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might return
180ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     * null when the given Drawable isn't BitmapDrawable, or if the system fails to create a scaled
181ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     * Bitmap for the Drawable.
182ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian     */
183ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
184ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      if (!(photo instanceof BitmapDrawable)) {
185ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian        return null;
186ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      }
187ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      int iconSize = context.getResources().getDimensionPixelSize(R.dimen.notification_icon_size);
188ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
189ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      int orgWidth = orgBitmap.getWidth();
190ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      int orgHeight = orgBitmap.getHeight();
191ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
192ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      // We want downscaled one only when the original icon is too big.
193ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      if (longerEdge > iconSize) {
194ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian        float ratio = ((float) longerEdge) / iconSize;
195ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian        int newWidth = (int) (orgWidth / ratio);
196ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian        int newHeight = (int) (orgHeight / ratio);
197ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian        // If the longer edge is much longer than the shorter edge, the latter may
198ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian        // become 0 which will cause a crash.
199ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian        if (newWidth <= 0 || newHeight <= 0) {
200fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian          LogUtil.w(
201fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian              "ContactsAsyncHelper.Worker.getPhotoIconWhenAppropriate",
202fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian              "Photo icon's width or height become 0.");
203ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian          return null;
204ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian        }
205ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian
206ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian        // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
207ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian        // should be smaller than the original.
208ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian        return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
209ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      } else {
210ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian        return orgBitmap;
211ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian      }
212ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian    }
213ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian  }
214ccca31529c07970e89419fb85a9e8153a5396838Eric Erfanian}
215