1 /* 2 * Copyright (C) 2017 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.server.slice; 18 19 import static android.app.usage.UsageEvents.Event.SLICE_PINNED; 20 import static android.app.usage.UsageEvents.Event.SLICE_PINNED_PRIV; 21 import static android.content.ContentProvider.getUriWithoutUserId; 22 import static android.content.ContentProvider.getUserIdFromUri; 23 import static android.content.ContentProvider.maybeAddUserId; 24 import static android.content.pm.PackageManager.PERMISSION_DENIED; 25 import static android.content.pm.PackageManager.PERMISSION_GRANTED; 26 import static android.os.Process.SYSTEM_UID; 27 28 import android.Manifest.permission; 29 import android.app.ActivityManager; 30 import android.app.AppOpsManager; 31 import android.app.ContentProviderHolder; 32 import android.app.IActivityManager; 33 import android.app.slice.ISliceManager; 34 import android.app.slice.SliceSpec; 35 import android.app.usage.UsageStatsManagerInternal; 36 import android.content.BroadcastReceiver; 37 import android.content.ComponentName; 38 import android.content.ContentProvider; 39 import android.content.ContentResolver; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.content.IntentFilter; 43 import android.content.pm.PackageManager; 44 import android.content.pm.PackageManagerInternal; 45 import android.content.pm.ResolveInfo; 46 import android.net.Uri; 47 import android.os.Binder; 48 import android.os.Handler; 49 import android.os.IBinder; 50 import android.os.Looper; 51 import android.os.Process; 52 import android.os.RemoteException; 53 import android.os.ResultReceiver; 54 import android.os.ShellCallback; 55 import android.os.UserHandle; 56 import android.util.ArrayMap; 57 import android.util.Slog; 58 import android.util.SparseArray; 59 import android.util.Xml.Encoding; 60 61 import com.android.internal.annotations.GuardedBy; 62 import com.android.internal.annotations.VisibleForTesting; 63 import com.android.internal.app.AssistUtils; 64 import com.android.internal.util.Preconditions; 65 import com.android.server.LocalServices; 66 import com.android.server.ServiceThread; 67 import com.android.server.SystemService; 68 69 import org.xmlpull.v1.XmlPullParser; 70 import org.xmlpull.v1.XmlPullParserException; 71 import org.xmlpull.v1.XmlPullParserFactory; 72 import org.xmlpull.v1.XmlSerializer; 73 74 import java.io.ByteArrayInputStream; 75 import java.io.ByteArrayOutputStream; 76 import java.io.FileDescriptor; 77 import java.io.IOException; 78 import java.util.ArrayList; 79 import java.util.List; 80 import java.util.Objects; 81 import java.util.function.Supplier; 82 83 public class SliceManagerService extends ISliceManager.Stub { 84 85 private static final String TAG = "SliceManagerService"; 86 private final Object mLock = new Object(); 87 88 private final Context mContext; 89 private final PackageManagerInternal mPackageManagerInternal; 90 private final AppOpsManager mAppOps; 91 private final AssistUtils mAssistUtils; 92 93 @GuardedBy("mLock") 94 private final ArrayMap<Uri, PinnedSliceState> mPinnedSlicesByUri = new ArrayMap<>(); 95 @GuardedBy("mLock") 96 private final SparseArray<PackageMatchingCache> mAssistantLookup = new SparseArray<>(); 97 @GuardedBy("mLock") 98 private final SparseArray<PackageMatchingCache> mHomeLookup = new SparseArray<>(); 99 private final Handler mHandler; 100 101 private final SlicePermissionManager mPermissions; 102 private final UsageStatsManagerInternal mAppUsageStats; 103 104 public SliceManagerService(Context context) { 105 this(context, createHandler().getLooper()); 106 } 107 108 @VisibleForTesting 109 SliceManagerService(Context context, Looper looper) { 110 mContext = context; 111 mPackageManagerInternal = Preconditions.checkNotNull( 112 LocalServices.getService(PackageManagerInternal.class)); 113 mAppOps = context.getSystemService(AppOpsManager.class); 114 mAssistUtils = new AssistUtils(context); 115 mHandler = new Handler(looper); 116 117 mAppUsageStats = LocalServices.getService(UsageStatsManagerInternal.class); 118 119 mPermissions = new SlicePermissionManager(mContext, looper); 120 121 IntentFilter filter = new IntentFilter(); 122 filter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED); 123 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 124 filter.addDataScheme("package"); 125 mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); 126 } 127 128 /// ----- Lifecycle stuff ----- 129 private void systemReady() { 130 } 131 132 private void onUnlockUser(int userId) { 133 } 134 135 private void onStopUser(int userId) { 136 synchronized (mLock) { 137 mPinnedSlicesByUri.values().removeIf(s -> getUserIdFromUri(s.getUri()) == userId); 138 } 139 } 140 141 /// ----- ISliceManager stuff ----- 142 @Override 143 public Uri[] getPinnedSlices(String pkg) { 144 verifyCaller(pkg); 145 int callingUser = Binder.getCallingUserHandle().getIdentifier(); 146 ArrayList<Uri> ret = new ArrayList<>(); 147 synchronized (mLock) { 148 for (PinnedSliceState state : mPinnedSlicesByUri.values()) { 149 if (Objects.equals(pkg, state.getPkg())) { 150 Uri uri = state.getUri(); 151 int userId = ContentProvider.getUserIdFromUri(uri, callingUser); 152 if (userId == callingUser) { 153 ret.add(ContentProvider.getUriWithoutUserId(uri)); 154 } 155 } 156 } 157 } 158 return ret.toArray(new Uri[ret.size()]); 159 } 160 161 @Override 162 public void pinSlice(String pkg, Uri uri, SliceSpec[] specs, IBinder token) 163 throws RemoteException { 164 verifyCaller(pkg); 165 enforceAccess(pkg, uri); 166 int user = Binder.getCallingUserHandle().getIdentifier(); 167 uri = maybeAddUserId(uri, user); 168 String slicePkg = getProviderPkg(uri, user); 169 getOrCreatePinnedSlice(uri, slicePkg).pin(pkg, specs, token); 170 171 mHandler.post(() -> { 172 if (slicePkg != null && !Objects.equals(pkg, slicePkg)) { 173 mAppUsageStats.reportEvent(slicePkg, user, 174 isAssistant(pkg, user) || isDefaultHomeApp(pkg, user) 175 ? SLICE_PINNED_PRIV : SLICE_PINNED); 176 } 177 }); 178 } 179 180 @Override 181 public void unpinSlice(String pkg, Uri uri, IBinder token) throws RemoteException { 182 verifyCaller(pkg); 183 enforceAccess(pkg, uri); 184 uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier()); 185 if (getPinnedSlice(uri).unpin(pkg, token)) { 186 removePinnedSlice(uri); 187 } 188 } 189 190 @Override 191 public boolean hasSliceAccess(String pkg) throws RemoteException { 192 verifyCaller(pkg); 193 return hasFullSliceAccess(pkg, Binder.getCallingUserHandle().getIdentifier()); 194 } 195 196 @Override 197 public SliceSpec[] getPinnedSpecs(Uri uri, String pkg) throws RemoteException { 198 verifyCaller(pkg); 199 enforceAccess(pkg, uri); 200 return getPinnedSlice(uri).getSpecs(); 201 } 202 203 @Override 204 public void grantSlicePermission(String pkg, String toPkg, Uri uri) throws RemoteException { 205 verifyCaller(pkg); 206 int user = Binder.getCallingUserHandle().getIdentifier(); 207 enforceOwner(pkg, uri, user); 208 mPermissions.grantSliceAccess(toPkg, user, pkg, user, uri); 209 } 210 211 @Override 212 public void revokeSlicePermission(String pkg, String toPkg, Uri uri) throws RemoteException { 213 verifyCaller(pkg); 214 int user = Binder.getCallingUserHandle().getIdentifier(); 215 enforceOwner(pkg, uri, user); 216 mPermissions.revokeSliceAccess(toPkg, user, pkg, user, uri); 217 } 218 219 @Override 220 public int checkSlicePermission(Uri uri, String pkg, int pid, int uid, 221 String[] autoGrantPermissions) { 222 int userId = UserHandle.getUserId(uid); 223 if (pkg == null) { 224 for (String p : mContext.getPackageManager().getPackagesForUid(uid)) { 225 if (checkSlicePermission(uri, p, pid, uid, autoGrantPermissions) 226 == PERMISSION_GRANTED) { 227 return PERMISSION_GRANTED; 228 } 229 } 230 return PERMISSION_DENIED; 231 } 232 if (hasFullSliceAccess(pkg, userId)) { 233 return PackageManager.PERMISSION_GRANTED; 234 } 235 if (mPermissions.hasPermission(pkg, userId, uri)) { 236 return PackageManager.PERMISSION_GRANTED; 237 } 238 if (autoGrantPermissions != null) { 239 // Need to own the Uri to call in with permissions to grant. 240 enforceOwner(pkg, uri, userId); 241 for (String perm : autoGrantPermissions) { 242 if (mContext.checkPermission(perm, pid, uid) == PERMISSION_GRANTED) { 243 int providerUser = ContentProvider.getUserIdFromUri(uri, userId); 244 String providerPkg = getProviderPkg(uri, providerUser); 245 mPermissions.grantSliceAccess(pkg, userId, providerPkg, providerUser, uri); 246 return PackageManager.PERMISSION_GRANTED; 247 } 248 } 249 } 250 // Fallback to allowing uri permissions through. 251 if (mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) 252 == PERMISSION_GRANTED) { 253 return PackageManager.PERMISSION_GRANTED; 254 } 255 return PackageManager.PERMISSION_DENIED; 256 } 257 258 @Override 259 public void grantPermissionFromUser(Uri uri, String pkg, String callingPkg, boolean allSlices) { 260 verifyCaller(callingPkg); 261 getContext().enforceCallingOrSelfPermission(permission.MANAGE_SLICE_PERMISSIONS, 262 "Slice granting requires MANAGE_SLICE_PERMISSIONS"); 263 int userId = Binder.getCallingUserHandle().getIdentifier(); 264 if (allSlices) { 265 mPermissions.grantFullAccess(pkg, userId); 266 } else { 267 // When granting, grant to all slices in the provider. 268 Uri grantUri = uri.buildUpon() 269 .path("") 270 .build(); 271 int providerUser = ContentProvider.getUserIdFromUri(grantUri, userId); 272 String providerPkg = getProviderPkg(grantUri, providerUser); 273 mPermissions.grantSliceAccess(pkg, userId, providerPkg, providerUser, grantUri); 274 } 275 long ident = Binder.clearCallingIdentity(); 276 try { 277 mContext.getContentResolver().notifyChange(uri, null); 278 } finally { 279 Binder.restoreCallingIdentity(ident); 280 } 281 } 282 283 // Backup/restore interface 284 @Override 285 public byte[] getBackupPayload(int user) { 286 if (Binder.getCallingUid() != SYSTEM_UID) { 287 throw new SecurityException("Caller must be system"); 288 } 289 //TODO: http://b/22388012 290 if (user != UserHandle.USER_SYSTEM) { 291 Slog.w(TAG, "getBackupPayload: cannot backup policy for user " + user); 292 return null; 293 } 294 final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 295 try { 296 XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer(); 297 out.setOutput(baos, Encoding.UTF_8.name()); 298 299 mPermissions.writeBackup(out); 300 301 out.flush(); 302 return baos.toByteArray(); 303 } catch (IOException | XmlPullParserException e) { 304 Slog.w(TAG, "getBackupPayload: error writing payload for user " + user, e); 305 } 306 return null; 307 } 308 309 @Override 310 public void applyRestore(byte[] payload, int user) { 311 if (Binder.getCallingUid() != SYSTEM_UID) { 312 throw new SecurityException("Caller must be system"); 313 } 314 if (payload == null) { 315 Slog.w(TAG, "applyRestore: no payload to restore for user " + user); 316 return; 317 } 318 //TODO: http://b/22388012 319 if (user != UserHandle.USER_SYSTEM) { 320 Slog.w(TAG, "applyRestore: cannot restore policy for user " + user); 321 return; 322 } 323 final ByteArrayInputStream bais = new ByteArrayInputStream(payload); 324 try { 325 XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser(); 326 parser.setInput(bais, Encoding.UTF_8.name()); 327 mPermissions.readRestore(parser); 328 } catch (NumberFormatException | XmlPullParserException | IOException e) { 329 Slog.w(TAG, "applyRestore: error reading payload", e); 330 } 331 } 332 333 @Override 334 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, 335 String[] args, ShellCallback callback, ResultReceiver resultReceiver) { 336 new SliceShellCommand(this).exec(this, in, out, err, args, callback, resultReceiver); 337 } 338 339 /// ----- internal code ----- 340 private void enforceOwner(String pkg, Uri uri, int user) { 341 if (!Objects.equals(getProviderPkg(uri, user), pkg) || pkg == null) { 342 throw new SecurityException("Caller must own " + uri); 343 } 344 } 345 346 protected void removePinnedSlice(Uri uri) { 347 synchronized (mLock) { 348 mPinnedSlicesByUri.remove(uri).destroy(); 349 } 350 } 351 352 private PinnedSliceState getPinnedSlice(Uri uri) { 353 synchronized (mLock) { 354 PinnedSliceState manager = mPinnedSlicesByUri.get(uri); 355 if (manager == null) { 356 throw new IllegalStateException(String.format("Slice %s not pinned", 357 uri.toString())); 358 } 359 return manager; 360 } 361 } 362 363 private PinnedSliceState getOrCreatePinnedSlice(Uri uri, String pkg) { 364 synchronized (mLock) { 365 PinnedSliceState manager = mPinnedSlicesByUri.get(uri); 366 if (manager == null) { 367 manager = createPinnedSlice(uri, pkg); 368 mPinnedSlicesByUri.put(uri, manager); 369 } 370 return manager; 371 } 372 } 373 374 @VisibleForTesting 375 protected PinnedSliceState createPinnedSlice(Uri uri, String pkg) { 376 return new PinnedSliceState(this, uri, pkg); 377 } 378 379 public Object getLock() { 380 return mLock; 381 } 382 383 public Context getContext() { 384 return mContext; 385 } 386 387 public Handler getHandler() { 388 return mHandler; 389 } 390 391 protected int checkAccess(String pkg, Uri uri, int uid, int pid) { 392 return checkSlicePermission(uri, pkg, uid, pid, null); 393 } 394 395 private String getProviderPkg(Uri uri, int user) { 396 long ident = Binder.clearCallingIdentity(); 397 try { 398 IBinder token = new Binder(); 399 IActivityManager activityManager = ActivityManager.getService(); 400 ContentProviderHolder holder = null; 401 String providerName = getUriWithoutUserId(uri).getAuthority(); 402 try { 403 try { 404 holder = activityManager.getContentProviderExternal( 405 providerName, getUserIdFromUri(uri, user), token); 406 if (holder != null && holder.info != null) { 407 return holder.info.packageName; 408 } else { 409 return null; 410 } 411 } finally { 412 if (holder != null && holder.provider != null) { 413 activityManager.removeContentProviderExternal(providerName, token); 414 } 415 } 416 } catch (RemoteException e) { 417 // Can't happen. 418 throw e.rethrowAsRuntimeException(); 419 } 420 } finally { 421 // I know, the double finally seems ugly, but seems safest for the identity. 422 Binder.restoreCallingIdentity(ident); 423 } 424 } 425 426 private void enforceCrossUser(String pkg, Uri uri) { 427 int user = Binder.getCallingUserHandle().getIdentifier(); 428 if (getUserIdFromUri(uri, user) != user) { 429 getContext().enforceCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS_FULL, 430 "Slice interaction across users requires INTERACT_ACROSS_USERS_FULL"); 431 } 432 } 433 434 private void enforceAccess(String pkg, Uri uri) throws RemoteException { 435 if (checkAccess(pkg, uri, Binder.getCallingUid(), Binder.getCallingPid()) 436 != PERMISSION_GRANTED) { 437 int userId = ContentProvider.getUserIdFromUri(uri, 438 Binder.getCallingUserHandle().getIdentifier()); 439 if (!Objects.equals(pkg, getProviderPkg(uri, userId))) { 440 throw new SecurityException("Access to slice " + uri + " is required"); 441 } 442 } 443 enforceCrossUser(pkg, uri); 444 } 445 446 private void verifyCaller(String pkg) { 447 mAppOps.checkPackage(Binder.getCallingUid(), pkg); 448 } 449 450 private boolean hasFullSliceAccess(String pkg, int userId) { 451 long ident = Binder.clearCallingIdentity(); 452 try { 453 boolean ret = isDefaultHomeApp(pkg, userId) || isAssistant(pkg, userId) 454 || isGrantedFullAccess(pkg, userId); 455 return ret; 456 } finally { 457 Binder.restoreCallingIdentity(ident); 458 } 459 } 460 461 private boolean isAssistant(String pkg, int userId) { 462 return getAssistantMatcher(userId).matches(pkg); 463 } 464 465 private boolean isDefaultHomeApp(String pkg, int userId) { 466 return getHomeMatcher(userId).matches(pkg); 467 } 468 469 private PackageMatchingCache getAssistantMatcher(int userId) { 470 PackageMatchingCache matcher = mAssistantLookup.get(userId); 471 if (matcher == null) { 472 matcher = new PackageMatchingCache(() -> getAssistant(userId)); 473 mAssistantLookup.put(userId, matcher); 474 } 475 return matcher; 476 } 477 478 private PackageMatchingCache getHomeMatcher(int userId) { 479 PackageMatchingCache matcher = mHomeLookup.get(userId); 480 if (matcher == null) { 481 matcher = new PackageMatchingCache(() -> getDefaultHome(userId)); 482 mHomeLookup.put(userId, matcher); 483 } 484 return matcher; 485 } 486 487 private String getAssistant(int userId) { 488 final ComponentName cn = mAssistUtils.getAssistComponentForUser(userId); 489 if (cn == null) { 490 return null; 491 } 492 return cn.getPackageName(); 493 } 494 495 // Based on getDefaultHome in ShortcutService. 496 // TODO: Unify if possible 497 @VisibleForTesting 498 protected String getDefaultHome(int userId) { 499 final long token = Binder.clearCallingIdentity(); 500 try { 501 final List<ResolveInfo> allHomeCandidates = new ArrayList<>(); 502 503 // Default launcher from package manager. 504 final ComponentName defaultLauncher = mPackageManagerInternal 505 .getHomeActivitiesAsUser(allHomeCandidates, userId); 506 507 ComponentName detected = null; 508 if (defaultLauncher != null) { 509 detected = defaultLauncher; 510 } 511 512 if (detected == null) { 513 // If we reach here, that means it's the first check since the user was created, 514 // and there's already multiple launchers and there's no default set. 515 // Find the system one with the highest priority. 516 // (We need to check the priority too because of FallbackHome in Settings.) 517 // If there's no system launcher yet, then no one can access slices, until 518 // the user explicitly sets one. 519 final int size = allHomeCandidates.size(); 520 521 int lastPriority = Integer.MIN_VALUE; 522 for (int i = 0; i < size; i++) { 523 final ResolveInfo ri = allHomeCandidates.get(i); 524 if (!ri.activityInfo.applicationInfo.isSystemApp()) { 525 continue; 526 } 527 if (ri.priority < lastPriority) { 528 continue; 529 } 530 detected = ri.activityInfo.getComponentName(); 531 lastPriority = ri.priority; 532 } 533 } 534 return detected != null ? detected.getPackageName() : null; 535 } finally { 536 Binder.restoreCallingIdentity(token); 537 } 538 } 539 540 private boolean isGrantedFullAccess(String pkg, int userId) { 541 return mPermissions.hasFullAccess(pkg, userId); 542 } 543 544 private static ServiceThread createHandler() { 545 ServiceThread handlerThread = new ServiceThread(TAG, 546 Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/); 547 handlerThread.start(); 548 return handlerThread; 549 } 550 551 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 552 @Override 553 public void onReceive(Context context, Intent intent) { 554 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); 555 if (userId == UserHandle.USER_NULL) { 556 Slog.w(TAG, "Intent broadcast does not contain user handle: " + intent); 557 return; 558 } 559 Uri data = intent.getData(); 560 String pkg = data != null ? data.getSchemeSpecificPart() : null; 561 if (pkg == null) { 562 Slog.w(TAG, "Intent broadcast does not contain package name: " + intent); 563 return; 564 } 565 switch (intent.getAction()) { 566 case Intent.ACTION_PACKAGE_REMOVED: 567 final boolean replacing = 568 intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); 569 if (!replacing) { 570 mPermissions.removePkg(pkg, userId); 571 } 572 break; 573 case Intent.ACTION_PACKAGE_DATA_CLEARED: 574 mPermissions.removePkg(pkg, userId); 575 break; 576 } 577 } 578 }; 579 580 public String[] getAllPackagesGranted(String authority) { 581 String pkg = getProviderPkg(new Uri.Builder() 582 .scheme(ContentResolver.SCHEME_CONTENT) 583 .authority(authority) 584 .build(), 0); 585 return mPermissions.getAllPackagesGranted(pkg); 586 } 587 588 /** 589 * Holder that caches a package that has access to a slice. 590 */ 591 static class PackageMatchingCache { 592 593 private final Supplier<String> mPkgSource; 594 private String mCurrentPkg; 595 596 public PackageMatchingCache(Supplier<String> pkgSource) { 597 mPkgSource = pkgSource; 598 } 599 600 public boolean matches(String pkgCandidate) { 601 if (pkgCandidate == null) return false; 602 603 if (Objects.equals(pkgCandidate, mCurrentPkg)) { 604 return true; 605 } 606 // Failed on cached value, try updating. 607 mCurrentPkg = mPkgSource.get(); 608 return Objects.equals(pkgCandidate, mCurrentPkg); 609 } 610 } 611 612 public static class Lifecycle extends SystemService { 613 private SliceManagerService mService; 614 615 public Lifecycle(Context context) { 616 super(context); 617 } 618 619 @Override 620 public void onStart() { 621 mService = new SliceManagerService(getContext()); 622 publishBinderService(Context.SLICE_SERVICE, mService); 623 } 624 625 @Override 626 public void onBootPhase(int phase) { 627 if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) { 628 mService.systemReady(); 629 } 630 } 631 632 @Override 633 public void onUnlockUser(int userHandle) { 634 mService.onUnlockUser(userHandle); 635 } 636 637 @Override 638 public void onStopUser(int userHandle) { 639 mService.onStopUser(userHandle); 640 } 641 } 642 643 private class SliceGrant { 644 private final Uri mUri; 645 private final String mPkg; 646 private final int mUserId; 647 648 public SliceGrant(Uri uri, String pkg, int userId) { 649 mUri = uri; 650 mPkg = pkg; 651 mUserId = userId; 652 } 653 654 @Override 655 public int hashCode() { 656 return mUri.hashCode() + mPkg.hashCode(); 657 } 658 659 @Override 660 public boolean equals(Object obj) { 661 if (!(obj instanceof SliceGrant)) return false; 662 SliceGrant other = (SliceGrant) obj; 663 return Objects.equals(other.mUri, mUri) && Objects.equals(other.mPkg, mPkg) 664 && (other.mUserId == mUserId); 665 } 666 } 667 } 668